Compare commits
88 Commits
1.12.0-Dev
...
zura-patch
| Author | SHA1 | Date | |
|---|---|---|---|
| f6134d1b7b | |||
| 50a5046c96 | |||
|
|
c0b8e15380 | ||
| e6735be594 | |||
|
|
fe419336d7 | ||
|
|
ffbeeba929 | ||
|
|
a7475a7007 | ||
|
|
3936cbd439 | ||
|
|
ba16963b66 | ||
| 6467a3e73b | |||
|
|
bb779904f7 | ||
|
|
59d0e8ee37 | ||
|
|
a441bbfcc8 | ||
|
|
c545ccea52 | ||
|
|
c32d9cadff | ||
|
|
d7c9df54cb | ||
| 37ec0961d9 | |||
| 9736c5090d | |||
|
|
4f3ab604db | ||
|
|
6a0f8c507c | ||
|
|
e13fde3d43 | ||
| 7b806ab660 | |||
|
|
387e5ad515 | ||
|
|
70c296a16b | ||
|
|
2a9b5812ed | ||
|
|
9b04976aa6 | ||
|
|
144ac166fb | ||
|
|
b06ffb3341 | ||
| e9461efe11 | |||
| 1f1afdec24 | |||
| d428a436e7 | |||
|
|
ad29fa7b69 | ||
|
|
23c56505ac | ||
|
|
58850f4530 | ||
|
|
f5339dc1d2 | ||
|
|
85ecea6391 | ||
|
|
cd817487e4 | ||
|
|
f50b622f0a | ||
|
|
d295f3e22d | ||
|
|
0dfa667ed3 | ||
|
|
2b118df892 | ||
|
|
f01229a97f | ||
|
|
3fdc9dd958 | ||
|
|
86107acf12 | ||
|
|
27e7fb7ed9 | ||
|
|
17f4ddad89 | ||
| a29e155cec | |||
|
|
1488704db4 | ||
|
|
46db5c87e0 | ||
| a772ee4705 | |||
|
|
1d88c04235 | ||
|
|
a7378652c4 | ||
| 61267d1b03 | |||
|
|
9b6d00570e | ||
|
|
83e4555e4b | ||
|
|
090b81c989 | ||
|
|
ca70c622bc | ||
|
|
49e5fb9d8d | ||
| a2bb1d7336 | |||
| 4f50028517 | |||
| 9f87a6a8fc | |||
|
|
4a391f2392 | ||
| a7c4b8f356 | |||
| c35650438c | |||
| b87185bc33 | |||
| 67da22fe9f | |||
|
|
15798e6753 | ||
| ca68e63c7d | |||
| eb10a27c6e | |||
| 39784a1fea | |||
|
|
fec2e4d380 | ||
|
|
afc3b4534c | ||
|
|
98b9cc7fe7 | ||
|
|
fd26d776a5 | ||
|
|
fdfd5722c7 | ||
|
|
19e42d34ff | ||
|
|
173e0aa7ae | ||
|
|
55d979b7c0 | ||
|
|
f31a139a3e | ||
| c82c633513 | |||
|
|
f6ea1eddc0 | ||
|
|
4ca3b6da48 | ||
| e75dd86fdb | |||
| 716d7a54d9 | |||
| 701ccaffe4 | |||
| ee8d05ca7a | |||
| fd5522b90a | |||
| 7672f147f5 |
@@ -41,9 +41,9 @@ jobs:
|
||||
|
||||
- name: Get version
|
||||
id: package_version
|
||||
uses: KageKirin/get-csproj-version@v0
|
||||
with:
|
||||
file: LightlessSync/LightlessSync.csproj
|
||||
run: |
|
||||
version=$(grep -oPm1 "(?<=<Version>)[^<]+" LightlessSync/LightlessSync.csproj)
|
||||
echo "version=$version" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Display version
|
||||
run: |
|
||||
@@ -121,8 +121,11 @@ jobs:
|
||||
"https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases"
|
||||
)
|
||||
|
||||
echo "API response: $response"
|
||||
release_id=$(echo "$response" | jq -r .id)
|
||||
echo "release_id=$release_id" >> "$GITHUB_OUTPUT"
|
||||
echo "release_id=$release_id"
|
||||
echo "release_id=$release_id" >> $GITHUB_OUTPUT || echo "::set-output name=release_id::$release_id"
|
||||
echo "RELEASE_ID=$release_id" >> $GITHUB_ENV
|
||||
|
||||
- name: Create Release (dev)
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
@@ -150,15 +153,28 @@ jobs:
|
||||
}' \
|
||||
"https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases"
|
||||
)
|
||||
echo "API response: $response"
|
||||
release_id=$(echo "$response" | jq -r .id)
|
||||
echo "release_id=$release_id" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload Assets to release
|
||||
echo "release_id=$release_id"
|
||||
echo "release_id=$release_id" >> $GITHUB_OUTPUT || echo "::set-output name=release_id::$release_id"
|
||||
echo "RELEASE_ID=$release_id" >> $GITHUB_ENV
|
||||
|
||||
- name: Check asset exists
|
||||
run: |
|
||||
if [ ! -f output/LightlessClient.zip ]; then
|
||||
echo "output/LightlessClient.zip does not exist!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload Assets to release
|
||||
env:
|
||||
RELEASE_ID: ${{ env.RELEASE_ID }}
|
||||
run: |
|
||||
echo "Uploading to release ID: $RELEASE_ID"
|
||||
curl --fail-with-body -s -X POST \
|
||||
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
||||
-F "attachment=@output/LightlessClient.zip" \
|
||||
"https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases/${{ steps.create_release.outputs.release_id }}/assets"
|
||||
"https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases/$RELEASE_ID/assets"
|
||||
|
||||
- name: Clone plugin hosting repo
|
||||
run: |
|
||||
@@ -241,6 +257,7 @@ jobs:
|
||||
updatedRepoJson=$(jq \
|
||||
--arg internalName "$internalName" \
|
||||
--arg dalamudApiLevel "$dalamudApiLevel" \
|
||||
--arg assemblyVersion "$assemblyVersion" \
|
||||
--arg version "$version" \
|
||||
--arg downloadUrl "$downloadUrl" \
|
||||
'
|
||||
|
||||
140
.github/workflows/lightless-tag-and-release.yml
vendored
140
.github/workflows/lightless-tag-and-release.yml
vendored
@@ -1,140 +0,0 @@
|
||||
name: Tag and Release Lightless
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
|
||||
env:
|
||||
PLUGIN_NAME: LightlessSync
|
||||
DOTNET_VERSION: 9.x
|
||||
|
||||
jobs:
|
||||
tag-and-release:
|
||||
runs-on: windows-2022
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout Lightless
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
|
||||
- name: Setup .NET 9 SDK
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.x
|
||||
|
||||
- name: Download Dalamud
|
||||
run: |
|
||||
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip
|
||||
Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev"
|
||||
|
||||
- name: Lets Build Lightless!
|
||||
run: |
|
||||
dotnet restore
|
||||
dotnet build --configuration Release --no-restore
|
||||
dotnet publish --configuration Release --no-build
|
||||
|
||||
- name: Get version
|
||||
id: package_version
|
||||
uses: KageKirin/get-csproj-version@v0
|
||||
with:
|
||||
file: LightlessSync/LightlessSync.csproj
|
||||
|
||||
- name: Display version
|
||||
run: |
|
||||
echo "Version: ${{ steps.package_version.outputs.version }}"
|
||||
|
||||
- name: Prepare Lightless Client
|
||||
run: |
|
||||
$publishPath = "${{ env.PLUGIN_NAME }}/bin/x64/Release/publish"
|
||||
if (Test-Path $publishPath) {
|
||||
Remove-Item -Recurse -Force $publishPath
|
||||
Write-Host "Removed $publishPath"
|
||||
} else {
|
||||
Write-Host "$publishPath does not exist, nothing to remove."
|
||||
}
|
||||
mkdir output
|
||||
Compress-Archive -Path ${{ env.PLUGIN_NAME }}/bin/x64/Release/* -DestinationPath output/LightlessClient.zip
|
||||
|
||||
- name: Create Git tag if not exists
|
||||
shell: pwsh
|
||||
run: |
|
||||
$tag = "${{ steps.package_version.outputs.version }}"
|
||||
git fetch --tags
|
||||
if (-not (git tag -l $tag)) {
|
||||
Write-Host "Tag $tag does not exist. Creating and pushing..."
|
||||
git config user.name "GitHub Action"
|
||||
git config user.email "action@github.com"
|
||||
git tag $tag
|
||||
git push origin $tag
|
||||
} else {
|
||||
Write-Host "Tag $tag already exists. Skipping tag creation."
|
||||
}
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ steps.package_version.outputs.version }}
|
||||
name: ${{ steps.package_version.outputs.version }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
files: output/LightlessClient.zip
|
||||
|
||||
- name: Clone plugin hosting repo
|
||||
run: |
|
||||
mkdir LightlessSyncRepo
|
||||
cd LightlessSyncRepo
|
||||
git clone https://github.com/${{ github.repository_owner }}/LightlessSync.git
|
||||
env:
|
||||
GIT_TERMINAL_PROMPT: 0
|
||||
|
||||
- name: Update plogonmaster.json with version
|
||||
shell: pwsh
|
||||
env:
|
||||
VERSION: ${{ steps.package_version.outputs.version }}
|
||||
run: |
|
||||
$pluginJsonPath = "${{ env.PLUGIN_NAME }}/bin/x64/Release/${{ env.PLUGIN_NAME }}.json"
|
||||
$pluginJson = Get-Content $pluginJsonPath | ConvertFrom-Json
|
||||
$repoJsonPath = "LightlessSyncRepo/LightlessSync/plogonmaster.json"
|
||||
$repoJsonRaw = Get-Content $repoJsonPath -Raw
|
||||
$repoJson = $repoJsonRaw | ConvertFrom-Json
|
||||
$version = $env:VERSION
|
||||
$downloadUrl = "https://github.com/${{ github.repository_owner }}/LightlessClient/releases/download/$version/LightlessClient.zip"
|
||||
|
||||
if (-not ($repoJson -is [System.Collections.IEnumerable])) {
|
||||
$repoJson = @($repoJson)
|
||||
}
|
||||
|
||||
foreach ($plugin in $repoJson) {
|
||||
if ($plugin.InternalName -eq $pluginJson.InternalName) {
|
||||
$plugin.DalamudApiLevel = $pluginJson.DalamudApiLevel
|
||||
$plugin.AssemblyVersion = $version
|
||||
$plugin.DownloadLinkInstall = $downloadUrl
|
||||
$plugin.DownloadLinkTesting = $downloadUrl
|
||||
$plugin.DownloadLinkUpdate = $downloadUrl
|
||||
}
|
||||
}
|
||||
|
||||
$repoJson | ConvertTo-Json -Depth 100 | Set-Content $repoJsonPath
|
||||
|
||||
# Convert to JSON and force array brackets if necessary
|
||||
$repoJsonString = $repoJson | ConvertTo-Json -Depth 100
|
||||
|
||||
# If the output is not an array, wrap it manually
|
||||
if ($repoJsonString.Trim().StartsWith('{')) {
|
||||
$repoJsonString = "[$repoJsonString]"
|
||||
}
|
||||
|
||||
$repoJsonString | Set-Content $repoJsonPath
|
||||
|
||||
- name: Commit and push to LightlessSync
|
||||
run: |
|
||||
cd LightlessSyncRepo/LightlessSync
|
||||
git config user.name "github-actions"
|
||||
git config user.email "github-actions@github.com"
|
||||
git add .
|
||||
git commit -m "Update ${{ env.PLUGIN_NAME }} to ${{ steps.package_version.outputs.version }}"
|
||||
git push https://x-access-token:${{ secrets.LIGHTLESS_TOKEN }}@github.com/${{ github.repository_owner }}/LightlessSync.git HEAD:main
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Penumbra-Sync
|
||||
Copyright (c) 2025 Lightless-Sync
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
Submodule LightlessAPI updated: aec2a5023e...44fbe10458
@@ -20,7 +20,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
private long _currentFileProgress = 0;
|
||||
private CancellationTokenSource _scanCancellationTokenSource = new();
|
||||
private readonly CancellationTokenSource _periodicCalculationTokenSource = new();
|
||||
public static readonly IImmutableList<string> AllowedFileExtensions = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk"];
|
||||
public static readonly IImmutableList<string> AllowedFileExtensions = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk", ".kdb"];
|
||||
|
||||
public CacheMonitor(ILogger<CacheMonitor> logger, IpcManager ipcManager, LightlessConfigService configService,
|
||||
FileCacheManager fileDbManager, LightlessMediator mediator, PerformanceCollectorService performanceCollector, DalamudUtilService dalamudUtil,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using K4os.Compression.LZ4.Legacy;
|
||||
using K4os.Compression.LZ4.Legacy;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.Services.Mediator;
|
||||
@@ -16,10 +16,13 @@ public sealed class FileCacheManager : IHostedService
|
||||
public const string CachePrefix = "{cache}";
|
||||
public const string CsvSplit = "|";
|
||||
public const string PenumbraPrefix = "{penumbra}";
|
||||
private const int FileCacheVersion = 1;
|
||||
private const string FileCacheVersionHeaderPrefix = "#lightless-file-cache-version:";
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
private readonly string _csvPath;
|
||||
private readonly ConcurrentDictionary<string, List<FileCacheEntity>> _fileCaches = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, FileCacheEntity>> _fileCaches = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, FileCacheEntity> _fileCachesByPrefixedPath = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly SemaphoreSlim _getCachesByPathsSemaphore = new(1, 1);
|
||||
private readonly Lock _fileWriteLock = new();
|
||||
private readonly IpcManager _ipcManager;
|
||||
@@ -37,12 +40,119 @@ public sealed class FileCacheManager : IHostedService
|
||||
|
||||
private string CsvBakPath => _csvPath + ".bak";
|
||||
|
||||
private static string NormalizeSeparators(string path)
|
||||
{
|
||||
return path.Replace("/", "\\", StringComparison.Ordinal)
|
||||
.Replace("\\\\", "\\", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string NormalizePrefixedPathKey(string prefixedPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(prefixedPath))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return NormalizeSeparators(prefixedPath).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static bool TryBuildPrefixedPath(string path, string? baseDirectory, string prefix, out string prefixedPath, out int matchedLength)
|
||||
{
|
||||
prefixedPath = string.Empty;
|
||||
matchedLength = 0;
|
||||
|
||||
if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(baseDirectory))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalizedPath = NormalizeSeparators(path).ToLowerInvariant();
|
||||
var normalizedBase = NormalizeSeparators(baseDirectory).TrimEnd('\\').ToLowerInvariant();
|
||||
|
||||
if (!normalizedPath.StartsWith(normalizedBase, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (normalizedPath.Length > normalizedBase.Length)
|
||||
{
|
||||
if (normalizedPath[normalizedBase.Length] != '\\')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
prefixedPath = prefix + normalizedPath.Substring(normalizedBase.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
prefixedPath = prefix;
|
||||
}
|
||||
|
||||
prefixedPath = prefixedPath.Replace("\\\\", "\\", StringComparison.Ordinal);
|
||||
matchedLength = normalizedBase.Length;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string BuildVersionHeader() => $"{FileCacheVersionHeaderPrefix}{FileCacheVersion}";
|
||||
|
||||
private static bool TryParseVersionHeader(string? line, out int version)
|
||||
{
|
||||
version = 0;
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!line.StartsWith(FileCacheVersionHeaderPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var versionSpan = line.AsSpan(FileCacheVersionHeaderPrefix.Length);
|
||||
return int.TryParse(versionSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out version);
|
||||
}
|
||||
|
||||
private string NormalizeToPrefixedPath(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path)) return string.Empty;
|
||||
|
||||
var normalized = NormalizeSeparators(path);
|
||||
|
||||
if (normalized.StartsWith(CachePrefix, StringComparison.OrdinalIgnoreCase) ||
|
||||
normalized.StartsWith(PenumbraPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return NormalizePrefixedPathKey(normalized);
|
||||
}
|
||||
|
||||
string? chosenPrefixed = null;
|
||||
var chosenLength = -1;
|
||||
|
||||
if (TryBuildPrefixedPath(normalized, _ipcManager.Penumbra.ModDirectory, PenumbraPrefix, out var penumbraPrefixed, out var penumbraMatch))
|
||||
{
|
||||
chosenPrefixed = penumbraPrefixed;
|
||||
chosenLength = penumbraMatch;
|
||||
}
|
||||
|
||||
if (TryBuildPrefixedPath(normalized, _configService.Current.CacheFolder, CachePrefix, out var cachePrefixed, out var cacheMatch))
|
||||
{
|
||||
if (cacheMatch > chosenLength)
|
||||
{
|
||||
chosenPrefixed = cachePrefixed;
|
||||
chosenLength = cacheMatch;
|
||||
}
|
||||
}
|
||||
|
||||
return NormalizePrefixedPathKey(chosenPrefixed ?? normalized);
|
||||
}
|
||||
|
||||
public FileCacheEntity? CreateCacheEntry(string path)
|
||||
{
|
||||
FileInfo fi = new(path);
|
||||
if (!fi.Exists) return null;
|
||||
_logger.LogTrace("Creating cache entry for {path}", path);
|
||||
return CreateFileEntity(_configService.Current.CacheFolder.ToLowerInvariant(), CachePrefix, fi);
|
||||
var cacheFolder = _configService.Current.CacheFolder;
|
||||
if (string.IsNullOrEmpty(cacheFolder)) return null;
|
||||
return CreateFileEntity(cacheFolder, CachePrefix, fi);
|
||||
}
|
||||
|
||||
public FileCacheEntity? CreateFileEntry(string path)
|
||||
@@ -50,31 +160,41 @@ public sealed class FileCacheManager : IHostedService
|
||||
FileInfo fi = new(path);
|
||||
if (!fi.Exists) return null;
|
||||
_logger.LogTrace("Creating file entry for {path}", path);
|
||||
return CreateFileEntity(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), PenumbraPrefix, fi);
|
||||
var modDirectory = _ipcManager.Penumbra.ModDirectory;
|
||||
if (string.IsNullOrEmpty(modDirectory)) return null;
|
||||
return CreateFileEntity(modDirectory, PenumbraPrefix, fi);
|
||||
}
|
||||
|
||||
private FileCacheEntity? CreateFileEntity(string directory, string prefix, FileInfo fi)
|
||||
{
|
||||
var fullName = fi.FullName.ToLowerInvariant();
|
||||
if (!fullName.Contains(directory, StringComparison.Ordinal)) return null;
|
||||
string prefixedPath = fullName.Replace(directory, prefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal);
|
||||
if (!TryBuildPrefixedPath(fi.FullName, directory, prefix, out var prefixedPath, out _))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return CreateFileCacheEntity(fi, prefixedPath);
|
||||
}
|
||||
|
||||
public List<FileCacheEntity> GetAllFileCaches() => _fileCaches.Values.SelectMany(v => v).ToList();
|
||||
public List<FileCacheEntity> GetAllFileCaches() => _fileCaches.Values.SelectMany(v => v.Values.Where(e => e != null)).ToList();
|
||||
|
||||
public List<FileCacheEntity> GetAllFileCachesByHash(string hash, bool ignoreCacheEntries = false, bool validate = true)
|
||||
{
|
||||
List<FileCacheEntity> output = [];
|
||||
if (_fileCaches.TryGetValue(hash, out var fileCacheEntities))
|
||||
{
|
||||
foreach (var fileCache in fileCacheEntities.Where(c => !ignoreCacheEntries || !c.IsCacheEntry).ToList())
|
||||
foreach (var fileCache in fileCacheEntities.Values.Where(c => !ignoreCacheEntries || !c.IsCacheEntry).ToList())
|
||||
{
|
||||
if (!validate) output.Add(fileCache);
|
||||
if (!validate)
|
||||
{
|
||||
output.Add(fileCache);
|
||||
}
|
||||
else
|
||||
{
|
||||
var validated = GetValidatedFileCache(fileCache);
|
||||
if (validated != null) output.Add(validated);
|
||||
if (validated != null)
|
||||
{
|
||||
output.Add(validated);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,7 +206,7 @@ public sealed class FileCacheManager : IHostedService
|
||||
{
|
||||
_lightlessMediator.Publish(new HaltScanMessage(nameof(ValidateLocalIntegrity)));
|
||||
_logger.LogInformation("Validating local storage");
|
||||
var cacheEntries = _fileCaches.SelectMany(v => v.Value).Where(v => v.IsCacheEntry).ToList();
|
||||
var cacheEntries = _fileCaches.Values.SelectMany(v => v.Values.Where(e => e != null)).Where(v => v.IsCacheEntry).ToList();
|
||||
List<FileCacheEntity> brokenEntities = [];
|
||||
int i = 0;
|
||||
foreach (var fileCache in cacheEntries)
|
||||
@@ -151,29 +271,40 @@ public sealed class FileCacheManager : IHostedService
|
||||
|
||||
public FileCacheEntity? GetFileCacheByHash(string hash)
|
||||
{
|
||||
if (_fileCaches.TryGetValue(hash, out var hashes))
|
||||
if (_fileCaches.TryGetValue(hash, out var entries))
|
||||
{
|
||||
var item = hashes.OrderBy(p => p.PrefixedFilePath.Contains(PenumbraPrefix, StringComparison.Ordinal) ? 0 : 1).FirstOrDefault();
|
||||
if (item != null) return GetValidatedFileCache(item);
|
||||
var item = entries.Values
|
||||
.OrderBy(p => p.PrefixedFilePath.Contains(PenumbraPrefix, StringComparison.Ordinal) ? 0 : 1)
|
||||
.FirstOrDefault();
|
||||
if (item != null)
|
||||
{
|
||||
return GetValidatedFileCache(item);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private FileCacheEntity? GetFileCacheByPath(string path)
|
||||
{
|
||||
var cleanedPath = path.Replace("/", "\\", StringComparison.OrdinalIgnoreCase).ToLowerInvariant()
|
||||
.Replace(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), "", StringComparison.OrdinalIgnoreCase);
|
||||
var entry = _fileCaches.SelectMany(v => v.Value).FirstOrDefault(f => f.ResolvedFilepath.EndsWith(cleanedPath, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (entry == null)
|
||||
var normalizedPrefixedPath = NormalizeToPrefixedPath(path);
|
||||
if (string.IsNullOrEmpty(normalizedPrefixedPath))
|
||||
{
|
||||
_logger.LogDebug("Found no entries for {path}", cleanedPath);
|
||||
return CreateFileEntry(path);
|
||||
return null;
|
||||
}
|
||||
|
||||
var validatedCacheEntry = GetValidatedFileCache(entry);
|
||||
if (_fileCachesByPrefixedPath.TryGetValue(normalizedPrefixedPath, out var entry))
|
||||
{
|
||||
return GetValidatedFileCache(entry);
|
||||
}
|
||||
|
||||
return validatedCacheEntry;
|
||||
_logger.LogDebug("Found no entries for {path}", normalizedPrefixedPath);
|
||||
|
||||
if (normalizedPrefixedPath.Contains(CachePrefix, StringComparison.Ordinal))
|
||||
{
|
||||
return CreateCacheEntry(path);
|
||||
}
|
||||
|
||||
return CreateFileEntry(path) ?? CreateCacheEntry(path);
|
||||
}
|
||||
|
||||
public Dictionary<string, FileCacheEntity?> GetFileCachesByPaths(string[] paths)
|
||||
@@ -182,66 +313,55 @@ public sealed class FileCacheManager : IHostedService
|
||||
|
||||
try
|
||||
{
|
||||
var allEntities = _fileCaches.SelectMany(f => f.Value).ToArray();
|
||||
var result = new Dictionary<string, FileCacheEntity?>(StringComparer.OrdinalIgnoreCase);
|
||||
var seenNormalized = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var cacheDict = new ConcurrentDictionary<string, FileCacheEntity>(
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
Parallel.ForEach(allEntities, entity =>
|
||||
foreach (var originalPath in paths)
|
||||
{
|
||||
cacheDict[entity.PrefixedFilePath] = entity;
|
||||
});
|
||||
|
||||
var cleanedPaths = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var seenCleaned = new ConcurrentDictionary<string, byte>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
Parallel.ForEach(paths, p =>
|
||||
{
|
||||
var cleaned = p.Replace("/", "\\", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace(
|
||||
_ipcManager.Penumbra.ModDirectory!,
|
||||
_ipcManager.Penumbra.ModDirectory!.EndsWith('\\')
|
||||
? PenumbraPrefix + '\\' : PenumbraPrefix,
|
||||
StringComparison.OrdinalIgnoreCase)
|
||||
.Replace(
|
||||
_configService.Current.CacheFolder,
|
||||
_configService.Current.CacheFolder.EndsWith('\\')
|
||||
? CachePrefix + '\\' : CachePrefix,
|
||||
StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("\\\\", "\\", StringComparison.Ordinal);
|
||||
|
||||
if (seenCleaned.TryAdd(cleaned, 0))
|
||||
if (string.IsNullOrEmpty(originalPath))
|
||||
{
|
||||
_logger.LogDebug("Adding to cleanedPaths: {cleaned}", cleaned);
|
||||
cleanedPaths[p] = cleaned;
|
||||
result[originalPath] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
var normalized = NormalizeToPrefixedPath(originalPath);
|
||||
if (seenNormalized.Add(normalized))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(normalized))
|
||||
{
|
||||
_logger.LogDebug("Normalized path {cleaned}", normalized);
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(normalized))
|
||||
{
|
||||
_logger.LogWarning("Duplicate normalized path detected: {cleaned}", normalized);
|
||||
}
|
||||
|
||||
if (_fileCachesByPrefixedPath.TryGetValue(normalized, out var entity))
|
||||
{
|
||||
result[originalPath] = GetValidatedFileCache(entity);
|
||||
continue;
|
||||
}
|
||||
|
||||
FileCacheEntity? created = null;
|
||||
|
||||
if (normalized.Contains(CachePrefix, StringComparison.Ordinal))
|
||||
{
|
||||
created = CreateCacheEntry(originalPath);
|
||||
}
|
||||
else if (normalized.Contains(PenumbraPrefix, StringComparison.Ordinal))
|
||||
{
|
||||
created = CreateFileEntry(originalPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Duplicate found: {cleaned}", cleaned);
|
||||
created = CreateFileEntry(originalPath) ?? CreateCacheEntry(originalPath);
|
||||
}
|
||||
});
|
||||
|
||||
var result = new ConcurrentDictionary<string, FileCacheEntity?>(StringComparer.OrdinalIgnoreCase);
|
||||
result[originalPath] = created;
|
||||
}
|
||||
|
||||
Parallel.ForEach(cleanedPaths, entry =>
|
||||
{
|
||||
_logger.LogDebug("Checking if in cache: {path}", entry.Value);
|
||||
|
||||
if (cacheDict.TryGetValue(entry.Value, out var entity))
|
||||
{
|
||||
var validatedCache = GetValidatedFileCache(entity);
|
||||
result[entry.Key] = validatedCache;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!entry.Value.Contains(CachePrefix, StringComparison.Ordinal))
|
||||
result[entry.Key] = CreateFileEntry(entry.Key);
|
||||
else
|
||||
result[entry.Key] = CreateCacheEntry(entry.Key);
|
||||
}
|
||||
});
|
||||
|
||||
return new Dictionary<string, FileCacheEntity?>(result, StringComparer.OrdinalIgnoreCase);
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -251,16 +371,24 @@ public sealed class FileCacheManager : IHostedService
|
||||
|
||||
public void RemoveHashedFile(string hash, string prefixedFilePath)
|
||||
{
|
||||
var normalizedPath = NormalizePrefixedPathKey(prefixedFilePath);
|
||||
|
||||
if (_fileCaches.TryGetValue(hash, out var caches))
|
||||
{
|
||||
var removedCount = caches?.RemoveAll(c => string.Equals(c.PrefixedFilePath, prefixedFilePath, StringComparison.Ordinal));
|
||||
_logger.LogTrace("Removed from DB: {count} file(s) with hash {hash} and file cache {path}", removedCount, hash, prefixedFilePath);
|
||||
_logger.LogTrace("Removing from DB: {hash} => {path}", hash, prefixedFilePath);
|
||||
|
||||
if (caches?.Count == 0)
|
||||
if (caches.TryRemove(normalizedPath, out var removedEntity))
|
||||
{
|
||||
_fileCaches.Remove(hash, out var entity);
|
||||
_logger.LogTrace("Removed from DB: {hash} => {path}", hash, removedEntity.PrefixedFilePath);
|
||||
}
|
||||
|
||||
if (caches.IsEmpty)
|
||||
{
|
||||
_fileCaches.TryRemove(hash, out _);
|
||||
}
|
||||
}
|
||||
|
||||
_fileCachesByPrefixedPath.TryRemove(normalizedPath, out _);
|
||||
}
|
||||
|
||||
public void UpdateHashedFile(FileCacheEntity fileCache, bool computeProperties = true)
|
||||
@@ -301,7 +429,8 @@ public sealed class FileCacheManager : IHostedService
|
||||
lock (_fileWriteLock)
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
foreach (var entry in _fileCaches.SelectMany(k => k.Value).OrderBy(f => f.PrefixedFilePath, StringComparer.OrdinalIgnoreCase))
|
||||
sb.AppendLine(BuildVersionHeader());
|
||||
foreach (var entry in _fileCaches.Values.SelectMany(k => k.Values).OrderBy(f => f.PrefixedFilePath, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
sb.AppendLine(entry.CsvEntry);
|
||||
}
|
||||
@@ -323,6 +452,53 @@ public sealed class FileCacheManager : IHostedService
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureCsvHeaderLocked()
|
||||
{
|
||||
if (!File.Exists(_csvPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string[] existingLines = File.ReadAllLines(_csvPath);
|
||||
if (existingLines.Length > 0 && TryParseVersionHeader(existingLines[0], out var existingVersion) && existingVersion == FileCacheVersion)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder rebuilt = new();
|
||||
rebuilt.AppendLine(BuildVersionHeader());
|
||||
foreach (var line in existingLines)
|
||||
{
|
||||
if (TryParseVersionHeader(line, out _))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(line))
|
||||
{
|
||||
rebuilt.AppendLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
File.WriteAllText(_csvPath, rebuilt.ToString());
|
||||
}
|
||||
|
||||
private void BackupUnsupportedCache(string suffix)
|
||||
{
|
||||
var sanitizedSuffix = string.IsNullOrWhiteSpace(suffix) ? "unsupported" : $"{suffix}.unsupported";
|
||||
var backupPath = _csvPath + "." + sanitizedSuffix;
|
||||
|
||||
try
|
||||
{
|
||||
File.Move(_csvPath, backupPath, overwrite: true);
|
||||
_logger.LogWarning("Backed up unsupported file cache to {path}", backupPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to back up unsupported file cache to {path}", backupPath);
|
||||
}
|
||||
}
|
||||
|
||||
internal FileCacheEntity MigrateFileHashToExtension(FileCacheEntity fileCache, string ext)
|
||||
{
|
||||
try
|
||||
@@ -346,16 +522,11 @@ public sealed class FileCacheManager : IHostedService
|
||||
|
||||
private void AddHashedFile(FileCacheEntity fileCache)
|
||||
{
|
||||
if (!_fileCaches.TryGetValue(fileCache.Hash, out var entries) || entries is null)
|
||||
{
|
||||
_fileCaches[fileCache.Hash] = entries = [];
|
||||
}
|
||||
var normalizedPath = NormalizePrefixedPathKey(fileCache.PrefixedFilePath);
|
||||
var entries = _fileCaches.GetOrAdd(fileCache.Hash, _ => new ConcurrentDictionary<string, FileCacheEntity>(StringComparer.OrdinalIgnoreCase));
|
||||
|
||||
if (!entries.Exists(u => string.Equals(u.PrefixedFilePath, fileCache.PrefixedFilePath, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
//_logger.LogTrace("Adding to DB: {hash} => {path}", fileCache.Hash, fileCache.PrefixedFilePath);
|
||||
entries.Add(fileCache);
|
||||
}
|
||||
entries[normalizedPath] = fileCache;
|
||||
_fileCachesByPrefixedPath[normalizedPath] = fileCache;
|
||||
}
|
||||
|
||||
private FileCacheEntity? CreateFileCacheEntity(FileInfo fileInfo, string prefixedPath, string? hash = null)
|
||||
@@ -366,7 +537,15 @@ public sealed class FileCacheManager : IHostedService
|
||||
AddHashedFile(entity);
|
||||
lock (_fileWriteLock)
|
||||
{
|
||||
File.AppendAllLines(_csvPath, new[] { entity.CsvEntry });
|
||||
if (!File.Exists(_csvPath))
|
||||
{
|
||||
File.WriteAllLines(_csvPath, new[] { BuildVersionHeader(), entity.CsvEntry });
|
||||
}
|
||||
else
|
||||
{
|
||||
EnsureCsvHeaderLocked();
|
||||
File.AppendAllLines(_csvPath, new[] { entity.CsvEntry });
|
||||
}
|
||||
}
|
||||
var result = GetFileCacheByPath(fileInfo.FullName);
|
||||
_logger.LogTrace("Creating cache entity for {name} success: {success}", fileInfo.FullName, (result != null));
|
||||
@@ -397,6 +576,12 @@ public sealed class FileCacheManager : IHostedService
|
||||
|
||||
private FileCacheEntity? Validate(FileCacheEntity fileCache)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileCache.ResolvedFilepath))
|
||||
{
|
||||
_logger.LogWarning("FileCacheEntity has empty ResolvedFilepath for hash {hash}, prefixed path {prefixed}", fileCache.Hash, fileCache.PrefixedFilePath);
|
||||
RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath);
|
||||
return null;
|
||||
}
|
||||
var file = new FileInfo(fileCache.ResolvedFilepath);
|
||||
if (!file.Exists)
|
||||
{
|
||||
@@ -479,49 +664,111 @@ public sealed class FileCacheManager : IHostedService
|
||||
_logger.LogWarning("Could not load entries from {path}, continuing with empty file cache", _csvPath);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Found {amount} files in {path}", entries.Length, _csvPath);
|
||||
bool rewriteRequired = false;
|
||||
bool parseEntries = entries.Length > 0;
|
||||
int startIndex = 0;
|
||||
|
||||
Dictionary<string, bool> processedFiles = new(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var entry in entries)
|
||||
if (entries.Length > 0)
|
||||
{
|
||||
var splittedEntry = entry.Split(CsvSplit, StringSplitOptions.None);
|
||||
try
|
||||
var headerLine = entries[0];
|
||||
var hasHeader = !string.IsNullOrEmpty(headerLine) &&
|
||||
headerLine.StartsWith(FileCacheVersionHeaderPrefix, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (hasHeader)
|
||||
{
|
||||
var hash = splittedEntry[0];
|
||||
if (hash.Length != 40) throw new InvalidOperationException("Expected Hash length of 40, received " + hash.Length);
|
||||
var path = splittedEntry[1];
|
||||
var time = splittedEntry[2];
|
||||
|
||||
if (processedFiles.ContainsKey(path))
|
||||
if (!TryParseVersionHeader(headerLine, out var parsedVersion))
|
||||
{
|
||||
_logger.LogWarning("Already processed {file}, ignoring", path);
|
||||
continue;
|
||||
_logger.LogWarning("Failed to parse file cache version header \"{header}\". Backing up existing cache.", headerLine);
|
||||
BackupUnsupportedCache("invalid-version");
|
||||
parseEntries = false;
|
||||
rewriteRequired = true;
|
||||
entries = Array.Empty<string>();
|
||||
}
|
||||
|
||||
processedFiles.Add(path, value: true);
|
||||
|
||||
long size = -1;
|
||||
long compressed = -1;
|
||||
if (splittedEntry.Length > 3)
|
||||
else if (parsedVersion != FileCacheVersion)
|
||||
{
|
||||
if (long.TryParse(splittedEntry[3], CultureInfo.InvariantCulture, out long result))
|
||||
{
|
||||
size = result;
|
||||
}
|
||||
if (long.TryParse(splittedEntry[4], CultureInfo.InvariantCulture, out long resultCompressed))
|
||||
{
|
||||
compressed = resultCompressed;
|
||||
}
|
||||
_logger.LogWarning("Unsupported file cache version {version} detected (expected {expected}). Backing up existing cache.", parsedVersion, FileCacheVersion);
|
||||
BackupUnsupportedCache($"v{parsedVersion}");
|
||||
parseEntries = false;
|
||||
rewriteRequired = true;
|
||||
entries = Array.Empty<string>();
|
||||
}
|
||||
else
|
||||
{
|
||||
startIndex = 1;
|
||||
}
|
||||
AddHashedFile(ReplacePathPrefixes(new FileCacheEntity(hash, path, time, size, compressed)));
|
||||
}
|
||||
catch (Exception ex)
|
||||
else if (entries.Length > 0)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to initialize entry {entry}, ignoring", entry);
|
||||
_logger.LogInformation("File cache missing version header, scheduling rewrite.");
|
||||
rewriteRequired = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (processedFiles.Count != entries.Length)
|
||||
var totalEntries = Math.Max(0, entries.Length - startIndex);
|
||||
Dictionary<string, bool> processedFiles = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (parseEntries && totalEntries > 0)
|
||||
{
|
||||
_logger.LogInformation("Found {amount} files in {path}", totalEntries, _csvPath);
|
||||
|
||||
for (var index = startIndex; index < entries.Length; index++)
|
||||
{
|
||||
var entry = entries[index];
|
||||
if (string.IsNullOrWhiteSpace(entry))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var splittedEntry = entry.Split(CsvSplit, StringSplitOptions.None);
|
||||
try
|
||||
{
|
||||
var hash = splittedEntry[0];
|
||||
if (hash.Length != 40)
|
||||
throw new InvalidOperationException("Expected Hash length of 40, received " + hash.Length);
|
||||
var path = splittedEntry[1];
|
||||
var time = splittedEntry[2];
|
||||
|
||||
if (processedFiles.ContainsKey(path))
|
||||
{
|
||||
_logger.LogWarning("Already processed {file}, ignoring", path);
|
||||
continue;
|
||||
}
|
||||
|
||||
processedFiles.Add(path, value: true);
|
||||
|
||||
long size = -1;
|
||||
long compressed = -1;
|
||||
if (splittedEntry.Length > 3)
|
||||
{
|
||||
if (long.TryParse(splittedEntry[3], CultureInfo.InvariantCulture, out long result))
|
||||
{
|
||||
size = result;
|
||||
}
|
||||
if (splittedEntry.Length > 4 &&
|
||||
long.TryParse(splittedEntry[4], CultureInfo.InvariantCulture, out long resultCompressed))
|
||||
{
|
||||
compressed = resultCompressed;
|
||||
}
|
||||
}
|
||||
AddHashedFile(ReplacePathPrefixes(new FileCacheEntity(hash, path, time, size, compressed)));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to initialize entry {entry}, ignoring", entry);
|
||||
}
|
||||
}
|
||||
|
||||
if (processedFiles.Count != totalEntries)
|
||||
{
|
||||
rewriteRequired = true;
|
||||
}
|
||||
}
|
||||
else if (!parseEntries && entries.Length > 0)
|
||||
{
|
||||
_logger.LogInformation("Skipping existing file cache entries due to incompatible version.");
|
||||
}
|
||||
|
||||
if (rewriteRequired)
|
||||
{
|
||||
WriteOutFullCsv();
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
private readonly HashSet<string> _cachedHandledPaths = new(StringComparer.Ordinal);
|
||||
private readonly TransientConfigService _configurationService;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly string[] _handledFileTypes = ["tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk"];
|
||||
private readonly string[] _handledFileTypes = ["tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk", "kdb"];
|
||||
private readonly string[] _handledRecordingFileTypes = ["tex", "mdl", "mtrl"];
|
||||
private readonly HashSet<GameObjectHandler> _playerRelatedPointers = [];
|
||||
private ConcurrentDictionary<IntPtr, ObjectKind> _cachedFrameAddresses = [];
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
public enum LightfinderDtrDisplayMode
|
||||
{
|
||||
NearbyBroadcasts = 0,
|
||||
PendingPairRequests = 1,
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using Dalamud.Game.Text;
|
||||
using LightlessSync.UtilsEnum.Enum;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.UI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -20,6 +22,13 @@ public class LightlessConfig : ILightlessConfiguration
|
||||
public DtrEntry.Colors DtrColorsDefault { get; set; } = default;
|
||||
public DtrEntry.Colors DtrColorsNotConnected { get; set; } = new(Glow: 0x0428FFu);
|
||||
public DtrEntry.Colors DtrColorsPairsInRange { get; set; } = new(Glow: 0xFFBA47u);
|
||||
public bool ShowLightfinderInDtr { get; set; } = false;
|
||||
public bool UseLightfinderColorsInDtr { get; set; } = true;
|
||||
public DtrEntry.Colors DtrColorsLightfinderEnabled { get; set; } = new(Foreground: 0xB590FFu, Glow: 0x4F406Eu);
|
||||
public DtrEntry.Colors DtrColorsLightfinderDisabled { get; set; } = new(Foreground: 0xD44444u, Glow: 0x642222u);
|
||||
public DtrEntry.Colors DtrColorsLightfinderCooldown { get; set; } = new(Foreground: 0xFFE97Au, Glow: 0x766C3Au);
|
||||
public DtrEntry.Colors DtrColorsLightfinderUnavailable { get; set; } = new(Foreground: 0x000000u, Glow: 0x000000u);
|
||||
public LightfinderDtrDisplayMode LightfinderDtrDisplayMode { get; set; } = LightfinderDtrDisplayMode.PendingPairRequests;
|
||||
public bool UseLightlessRedesign { get; set; } = true;
|
||||
public bool EnableRightClickMenus { get; set; } = true;
|
||||
public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both;
|
||||
@@ -33,6 +42,9 @@ public class LightlessConfig : ILightlessConfiguration
|
||||
public bool OpenGposeImportOnGposeStart { get; set; } = false;
|
||||
public bool OpenPopupOnAdd { get; set; } = true;
|
||||
public int ParallelDownloads { get; set; } = 10;
|
||||
public int ParallelUploads { get; set; } = 8;
|
||||
public bool EnablePairProcessingLimiter { get; set; } = true;
|
||||
public int MaxConcurrentPairApplications { get; set; } = 3;
|
||||
public int DownloadSpeedLimitInBytes { get; set; } = 0;
|
||||
public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps;
|
||||
public bool PreferNotesOverNamesForVisible { get; set; } = false;
|
||||
@@ -51,6 +63,7 @@ public class LightlessConfig : ILightlessConfiguration
|
||||
public bool ShowOnlineNotificationsOnlyForNamedPairs { get; set; } = false;
|
||||
public bool ShowTransferBars { get; set; } = true;
|
||||
public bool ShowTransferWindow { get; set; } = false;
|
||||
public bool UseNotificationsForDownloads { get; set; } = true;
|
||||
public bool ShowUploading { get; set; } = true;
|
||||
public bool ShowUploadingBigText { get; set; } = true;
|
||||
public bool ShowVisibleUsersSeparately { get; set; } = true;
|
||||
@@ -64,11 +77,67 @@ public class LightlessConfig : ILightlessConfiguration
|
||||
public bool AutoPopulateEmptyNotesFromCharaName { get; set; } = false;
|
||||
public int Version { get; set; } = 1;
|
||||
public NotificationLocation WarningNotification { get; set; } = NotificationLocation.Both;
|
||||
|
||||
// Lightless Notification Configuration
|
||||
public bool UseLightlessNotifications { get; set; } = true;
|
||||
public bool ShowNotificationProgress { get; set; } = true;
|
||||
public NotificationLocation LightlessInfoNotification { get; set; } = NotificationLocation.LightlessUi;
|
||||
public NotificationLocation LightlessWarningNotification { get; set; } = NotificationLocation.LightlessUi;
|
||||
public NotificationLocation LightlessErrorNotification { get; set; } = NotificationLocation.ChatAndLightlessUi;
|
||||
public NotificationLocation LightlessPairRequestNotification { get; set; } = NotificationLocation.LightlessUi;
|
||||
public NotificationLocation LightlessDownloadNotification { get; set; } = NotificationLocation.TextOverlay;
|
||||
|
||||
// Basic Settings
|
||||
public float NotificationOpacity { get; set; } = 0.95f;
|
||||
public int MaxSimultaneousNotifications { get; set; } = 5;
|
||||
public bool AutoDismissOnAction { get; set; } = true;
|
||||
public bool DismissNotificationOnClick { get; set; } = false;
|
||||
public bool ShowNotificationTimestamp { get; set; } = false;
|
||||
|
||||
// Position & Layout
|
||||
public int NotificationOffsetY { get; set; } = 50;
|
||||
public int NotificationOffsetX { get; set; } = 0;
|
||||
public float NotificationWidth { get; set; } = 350f;
|
||||
public float NotificationSpacing { get; set; } = 8f;
|
||||
|
||||
// Animation & Effects
|
||||
public float NotificationAnimationSpeed { get; set; } = 10f;
|
||||
public float NotificationAccentBarWidth { get; set; } = 3f;
|
||||
|
||||
// Duration per Type
|
||||
public int InfoNotificationDurationSeconds { get; set; } = 10;
|
||||
public int WarningNotificationDurationSeconds { get; set; } = 15;
|
||||
public int ErrorNotificationDurationSeconds { get; set; } = 20;
|
||||
public int PairRequestDurationSeconds { get; set; } = 180;
|
||||
public int DownloadNotificationDurationSeconds { get; set; } = 300;
|
||||
public uint CustomInfoSoundId { get; set; } = 2; // Se2
|
||||
public uint CustomWarningSoundId { get; set; } = 16; // Se15
|
||||
public uint CustomErrorSoundId { get; set; } = 16; // Se15
|
||||
public uint PairRequestSoundId { get; set; } = 5; // Se5
|
||||
public uint DownloadSoundId { get; set; } = 15; // Se14
|
||||
public bool DisableInfoSound { get; set; } = true;
|
||||
public bool DisableWarningSound { get; set; } = true;
|
||||
public bool DisableErrorSound { get; set; } = true;
|
||||
public bool DisablePairRequestSound { get; set; } = true;
|
||||
public bool DisableDownloadSound { get; set; } = true;
|
||||
public bool UseFocusTarget { get; set; } = false;
|
||||
public bool overrideFriendColor { get; set; } = false;
|
||||
public bool overridePartyColor { get; set; } = false;
|
||||
public bool overrideFcTagColor { get; set; } = false;
|
||||
public bool useColoredUIDs { get; set; } = true;
|
||||
public bool BroadcastEnabled { get; set; } = false;
|
||||
public bool LightfinderAutoEnableOnConnect { get; set; } = false;
|
||||
public short LightfinderLabelOffsetX { get; set; } = 0;
|
||||
public short LightfinderLabelOffsetY { get; set; } = 0;
|
||||
public bool LightfinderLabelUseIcon { get; set; } = false;
|
||||
public bool LightfinderLabelShowOwn { get; set; } = true;
|
||||
public bool LightfinderLabelShowPaired { get; set; } = true;
|
||||
public bool LightfinderLabelShowHidden { get; set; } = false;
|
||||
public string LightfinderLabelIconGlyph { get; set; } = SeIconCharExtensions.ToIconString(SeIconChar.Hyadelyn);
|
||||
public float LightfinderLabelScale { get; set; } = 1.0f;
|
||||
public bool LightfinderAutoAlign { get; set; } = true;
|
||||
public LabelAlignment LabelAlignment { get; set; } = LabelAlignment.Left;
|
||||
public DateTime BroadcastTtl { get; set; } = DateTime.MinValue;
|
||||
public bool SyncshellFinderEnabled { get; set; } = false;
|
||||
public string? SelectedFinderSyncshell { get; set; } = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
|
||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
public class ServerTagConfig : ILightlessConfiguration
|
||||
{
|
||||
public Dictionary<string, ServerTagStorage> ServerTagStorage { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
public int Version { get; set; } = 0;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
[Serializable]
|
||||
public class UiStyleOverride
|
||||
{
|
||||
public uint? Color { get; set; }
|
||||
public float? Float { get; set; }
|
||||
public Vector2Config? Vector2 { get; set; }
|
||||
|
||||
public bool IsEmpty => Color is null && Float is null && Vector2 is null;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public record struct Vector2Config(float X, float Y)
|
||||
{
|
||||
public static implicit operator Vector2(Vector2Config value) => new(value.X, value.Y);
|
||||
public static implicit operator Vector2Config(Vector2 value) => new(value.X, value.Y);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
[Serializable]
|
||||
public class UiThemeConfig : ILightlessConfiguration
|
||||
{
|
||||
public Dictionary<string, UiStyleOverride> StyleOverrides { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public int Version { get; set; } = 1;
|
||||
}
|
||||
@@ -1,16 +1,21 @@
|
||||
namespace LightlessSync.LightlessConfiguration.Models;
|
||||
namespace LightlessSync.LightlessConfiguration.Models;
|
||||
|
||||
public enum NotificationLocation
|
||||
{
|
||||
Nowhere,
|
||||
Chat,
|
||||
Toast,
|
||||
Both
|
||||
Both,
|
||||
LightlessUi,
|
||||
ChatAndLightlessUi,
|
||||
TextOverlay,
|
||||
}
|
||||
|
||||
public enum NotificationType
|
||||
{
|
||||
Info,
|
||||
Warning,
|
||||
Error
|
||||
Error,
|
||||
PairRequest,
|
||||
Download
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace LightlessSync.LightlessConfiguration.Models;
|
||||
|
||||
[Serializable]
|
||||
public class ServerTagStorage
|
||||
{
|
||||
public HashSet<string> OpenPairTags { get; set; } = new(StringComparer.Ordinal);
|
||||
public HashSet<string> ServerAvailablePairTags { get; set; } = new(StringComparer.Ordinal);
|
||||
public Dictionary<string, List<string>> UidServerPairedUserTags { get; set; } = new(StringComparer.Ordinal);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
namespace LightlessSync.LightlessConfiguration;
|
||||
|
||||
public class ServerTagConfigService : ConfigurationServiceBase<ServerTagConfig>
|
||||
{
|
||||
public const string ConfigName = "servertags.json";
|
||||
|
||||
public ServerTagConfigService(string configDir) : base(configDir)
|
||||
{
|
||||
}
|
||||
|
||||
public override string ConfigurationName => ConfigName;
|
||||
}
|
||||
14
LightlessSync/LightlessConfiguration/UiThemeConfigService.cs
Normal file
14
LightlessSync/LightlessConfiguration/UiThemeConfigService.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
namespace LightlessSync.LightlessConfiguration;
|
||||
|
||||
public class UiThemeConfigService : ConfigurationServiceBase<UiThemeConfig>
|
||||
{
|
||||
public const string ConfigName = "ui-theme.json";
|
||||
|
||||
public UiThemeConfigService(string configDir) : base(configDir)
|
||||
{
|
||||
}
|
||||
|
||||
public override string ConfigurationName => ConfigName;
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors></Authors>
|
||||
<Company></Company>
|
||||
<Version>1.12.0</Version>
|
||||
<Version>1.12.2</Version>
|
||||
<Description></Description>
|
||||
<Copyright></Copyright>
|
||||
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
@@ -21,6 +21,7 @@ public class PairHandlerFactory
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
private readonly PlayerPerformanceService _playerPerformanceService;
|
||||
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||
private readonly ServerConfigurationManager _serverConfigManager;
|
||||
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
||||
|
||||
@@ -28,6 +29,7 @@ public class PairHandlerFactory
|
||||
FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService,
|
||||
PluginWarningNotificationService pluginWarningNotificationManager, IHostApplicationLifetime hostApplicationLifetime,
|
||||
FileCacheManager fileCacheManager, LightlessMediator lightlessMediator, PlayerPerformanceService playerPerformanceService,
|
||||
PairProcessingLimiter pairProcessingLimiter,
|
||||
ServerConfigurationManager serverConfigManager)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
@@ -40,6 +42,7 @@ public class PairHandlerFactory
|
||||
_fileCacheManager = fileCacheManager;
|
||||
_lightlessMediator = lightlessMediator;
|
||||
_playerPerformanceService = playerPerformanceService;
|
||||
_pairProcessingLimiter = pairProcessingLimiter;
|
||||
_serverConfigManager = serverConfigManager;
|
||||
}
|
||||
|
||||
@@ -47,6 +50,6 @@ public class PairHandlerFactory
|
||||
{
|
||||
return new PairHandler(_loggerFactory.CreateLogger<PairHandler>(), pair, _gameObjectHandlerFactory,
|
||||
_ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime,
|
||||
_fileCacheManager, _lightlessMediator, _playerPerformanceService, _serverConfigManager);
|
||||
_fileCacheManager, _lightlessMediator, _playerPerformanceService, _pairProcessingLimiter, _serverConfigManager);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.PlayerData.Factories;
|
||||
@@ -28,6 +28,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
||||
private readonly IpcManager _ipcManager;
|
||||
private readonly IHostApplicationLifetime _lifetime;
|
||||
private readonly PlayerPerformanceService _playerPerformanceService;
|
||||
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||
private readonly ServerConfigurationManager _serverConfigManager;
|
||||
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
|
||||
private CancellationTokenSource? _applicationCancellationTokenSource = new();
|
||||
@@ -50,6 +51,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
||||
DalamudUtilService dalamudUtil, IHostApplicationLifetime lifetime,
|
||||
FileCacheManager fileDbManager, LightlessMediator mediator,
|
||||
PlayerPerformanceService playerPerformanceService,
|
||||
PairProcessingLimiter pairProcessingLimiter,
|
||||
ServerConfigurationManager serverConfigManager) : base(logger, mediator)
|
||||
{
|
||||
Pair = pair;
|
||||
@@ -61,6 +63,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
||||
_lifetime = lifetime;
|
||||
_fileDbManager = fileDbManager;
|
||||
_playerPerformanceService = playerPerformanceService;
|
||||
_pairProcessingLimiter = pairProcessingLimiter;
|
||||
_serverConfigManager = serverConfigManager;
|
||||
_penumbraCollection = _ipcManager.Penumbra.CreateTemporaryCollectionAsync(logger, Pair.UserData.UID).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
|
||||
@@ -420,6 +423,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
||||
private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData,
|
||||
bool updateModdedPaths, bool updateManip, CancellationToken downloadToken)
|
||||
{
|
||||
await using var concurrencyLease = await _pairProcessingLimiter.AcquireAsync(downloadToken).ConfigureAwait(false);
|
||||
Dictionary<(string GamePath, string? Hash), string> moddedPaths = [];
|
||||
|
||||
if (updateModdedPaths)
|
||||
@@ -737,6 +741,11 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Logger.LogTrace("[BASE-{appBase}] Modded path calculation cancelled", applicationBase);
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "[BASE-{appBase}] Something went wrong during calculation replacements", applicationBase);
|
||||
@@ -763,4 +772,4 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
|
||||
_dataReceivedInDowntime = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Plugin.Services;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Comparer;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
@@ -7,10 +7,14 @@ using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.PlayerData.Factories;
|
||||
using LightlessSync.Services;
|
||||
|
||||
using LightlessSync.Services.Events;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
@@ -24,14 +28,19 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
||||
private Lazy<List<Pair>> _directPairsInternal;
|
||||
private Lazy<Dictionary<GroupFullInfoDto, List<Pair>>> _groupPairsInternal;
|
||||
private Lazy<Dictionary<Pair, List<GroupFullInfoDto>>> _pairsWithGroupsInternal;
|
||||
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||
private readonly ConcurrentQueue<(Pair Pair, OnlineUserIdentDto? Ident)> _pairCreationQueue = new();
|
||||
private CancellationTokenSource _pairCreationCts = new();
|
||||
private int _pairCreationProcessorRunning;
|
||||
|
||||
public PairManager(ILogger<PairManager> logger, PairFactory pairFactory,
|
||||
LightlessConfigService configurationService, LightlessMediator mediator,
|
||||
IContextMenu dalamudContextMenu) : base(logger, mediator)
|
||||
IContextMenu dalamudContextMenu, PairProcessingLimiter pairProcessingLimiter) : base(logger, mediator)
|
||||
{
|
||||
_pairFactory = pairFactory;
|
||||
_configurationService = configurationService;
|
||||
_dalamudContextMenu = dalamudContextMenu;
|
||||
_pairProcessingLimiter = pairProcessingLimiter;
|
||||
Mediator.Subscribe<DisconnectedMessage>(this, (_) => ClearPairs());
|
||||
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => ReapplyPairData());
|
||||
_directPairsInternal = DirectPairsLazy();
|
||||
@@ -112,6 +121,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
||||
public void ClearPairs()
|
||||
{
|
||||
Logger.LogDebug("Clearing all Pairs");
|
||||
ResetPairCreationQueue();
|
||||
DisposePairs();
|
||||
_allClientPairs.Clear();
|
||||
_allGroups.Clear();
|
||||
@@ -161,7 +171,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
||||
Mediator.Publish(new NotificationMessage("User online", msg, NotificationType.Info, TimeSpan.FromSeconds(5)));
|
||||
}
|
||||
|
||||
pair.CreateCachedPlayer(dto);
|
||||
QueuePairCreation(pair, dto);
|
||||
|
||||
RecreateLazy();
|
||||
}
|
||||
@@ -332,6 +342,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
ResetPairCreationQueue();
|
||||
_dalamudContextMenu.OnMenuOpened -= DalamudContextMenuOnOnOpenGameObjectContextMenu;
|
||||
|
||||
DisposePairs();
|
||||
@@ -390,6 +401,84 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
||||
});
|
||||
}
|
||||
|
||||
private void QueuePairCreation(Pair pair, OnlineUserIdentDto? dto)
|
||||
{
|
||||
if (pair.HasCachedPlayer)
|
||||
{
|
||||
RecreateLazy();
|
||||
return;
|
||||
}
|
||||
|
||||
_pairCreationQueue.Enqueue((pair, dto));
|
||||
StartPairCreationProcessor();
|
||||
}
|
||||
|
||||
private void StartPairCreationProcessor()
|
||||
{
|
||||
if (_pairCreationCts.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Interlocked.CompareExchange(ref _pairCreationProcessorRunning, 1, 0) == 0)
|
||||
{
|
||||
_ = Task.Run(ProcessPairCreationQueueAsync);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessPairCreationQueueAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!_pairCreationCts.IsCancellationRequested)
|
||||
{
|
||||
if (!_pairCreationQueue.TryDequeue(out var work))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var lease = await _pairProcessingLimiter.AcquireAsync(_pairCreationCts.Token).ConfigureAwait(false);
|
||||
if (!work.Pair.HasCachedPlayer)
|
||||
{
|
||||
work.Pair.CreateCachedPlayer(work.Ident);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error creating cached player for {uid}", work.Pair.UserData.UID);
|
||||
}
|
||||
|
||||
RecreateLazy();
|
||||
await Task.Yield();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref _pairCreationProcessorRunning, 0);
|
||||
if (!_pairCreationQueue.IsEmpty && !_pairCreationCts.IsCancellationRequested)
|
||||
{
|
||||
StartPairCreationProcessor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetPairCreationQueue()
|
||||
{
|
||||
_pairCreationCts.Cancel();
|
||||
while (_pairCreationQueue.TryDequeue(out _))
|
||||
{
|
||||
}
|
||||
_pairCreationCts.Dispose();
|
||||
_pairCreationCts = new CancellationTokenSource();
|
||||
Interlocked.Exchange(ref _pairCreationProcessorRunning, 0);
|
||||
}
|
||||
|
||||
private void ReapplyPairData()
|
||||
{
|
||||
foreach (var pair in _allClientPairs.Select(k => k.Value))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Utils;
|
||||
@@ -101,6 +101,8 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
forced |= _uploadingCharacterData?.DataHash != _lastCreatedData.DataHash;
|
||||
|
||||
if (_fileUploadTask == null || (_fileUploadTask?.IsCompleted ?? false) || forced)
|
||||
@@ -127,6 +129,15 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
|
||||
_pushDataSemaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (_runtimeCts.IsCancellationRequested)
|
||||
{
|
||||
Logger.LogDebug("PushCharacterData cancelled");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to push character data");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Interface.ImGuiFileDialog;
|
||||
using Dalamud.Interface.Windowing;
|
||||
@@ -106,6 +106,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
collection.AddSingleton<GameObjectHandlerFactory>();
|
||||
collection.AddSingleton<FileDownloadManagerFactory>();
|
||||
collection.AddSingleton<PairHandlerFactory>();
|
||||
collection.AddSingleton<PairProcessingLimiter>();
|
||||
collection.AddSingleton<PairFactory>();
|
||||
collection.AddSingleton<XivDataAnalyzer>();
|
||||
collection.AddSingleton<CharacterAnalyzer>();
|
||||
@@ -113,6 +114,8 @@ public sealed class Plugin : IDalamudPlugin
|
||||
collection.AddSingleton<PluginWarningNotificationService>();
|
||||
collection.AddSingleton<FileCompactor>();
|
||||
collection.AddSingleton<TagHandler>();
|
||||
collection.AddSingleton(s => new Lazy<ApiController>(() => s.GetRequiredService<ApiController>()));
|
||||
collection.AddSingleton<PairRequestService>();
|
||||
collection.AddSingleton<IdDisplayHandler>();
|
||||
collection.AddSingleton<PlayerPerformanceService>();
|
||||
collection.AddSingleton<TransientResourceManager>();
|
||||
@@ -140,14 +143,26 @@ public sealed class Plugin : IDalamudPlugin
|
||||
clientState, objectTable, framework, gameGui, condition, gameData, targetManager, gameConfig,
|
||||
s.GetRequiredService<BlockedCharacterHandler>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(),
|
||||
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<PlayerPerformanceConfigService>()));
|
||||
collection.AddSingleton((s) => new DtrEntry(s.GetRequiredService<ILogger<DtrEntry>>(), dtrBar, s.GetRequiredService<LightlessConfigService>(),
|
||||
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PairManager>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<ServerConfigurationManager>()));
|
||||
collection.AddSingleton((s) => new DtrEntry(
|
||||
s.GetRequiredService<ILogger<DtrEntry>>(),
|
||||
dtrBar,
|
||||
s.GetRequiredService<LightlessConfigService>(),
|
||||
s.GetRequiredService<LightlessMediator>(),
|
||||
s.GetRequiredService<PairManager>(),
|
||||
s.GetRequiredService<PairRequestService>(),
|
||||
s.GetRequiredService<ApiController>(),
|
||||
s.GetRequiredService<ServerConfigurationManager>(),
|
||||
s.GetRequiredService<BroadcastService>(),
|
||||
s.GetRequiredService<BroadcastScannerService>(),
|
||||
s.GetRequiredService<DalamudUtilService>()));
|
||||
collection.AddSingleton(s => new PairManager(s.GetRequiredService<ILogger<PairManager>>(), s.GetRequiredService<PairFactory>(),
|
||||
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), contextMenu));
|
||||
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), contextMenu, s.GetRequiredService<PairProcessingLimiter>()));
|
||||
collection.AddSingleton<RedrawManager>();
|
||||
collection.AddSingleton<BroadcastService>();
|
||||
collection.AddSingleton(addonLifecycle);
|
||||
collection.AddSingleton(p => new ContextMenu(contextMenu, pluginInterface, gameData, p.GetRequiredService<ILogger<ContextMenu>>(), p.GetRequiredService<DalamudUtilService>(), p.GetRequiredService<ApiController>(), objectTable));
|
||||
collection.AddSingleton(p => new ContextMenuService(contextMenu, pluginInterface, gameData,
|
||||
p.GetRequiredService<ILogger<ContextMenuService>>(), p.GetRequiredService<DalamudUtilService>(), p.GetRequiredService<ApiController>(), objectTable,
|
||||
p.GetRequiredService<LightlessConfigService>(), p.GetRequiredService<PairRequestService>(), p.GetRequiredService<PairManager>(), clientState));
|
||||
collection.AddSingleton((s) => new IpcCallerPenumbra(s.GetRequiredService<ILogger<IpcCallerPenumbra>>(), pluginInterface,
|
||||
s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<RedrawManager>()));
|
||||
collection.AddSingleton((s) => new IpcCallerGlamourer(s.GetRequiredService<ILogger<IpcCallerGlamourer>>(), pluginInterface,
|
||||
@@ -168,9 +183,14 @@ public sealed class Plugin : IDalamudPlugin
|
||||
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<IpcCallerPenumbra>(), s.GetRequiredService<IpcCallerGlamourer>(),
|
||||
s.GetRequiredService<IpcCallerCustomize>(), s.GetRequiredService<IpcCallerHeels>(), s.GetRequiredService<IpcCallerHonorific>(),
|
||||
s.GetRequiredService<IpcCallerMoodles>(), s.GetRequiredService<IpcCallerPetNames>(), s.GetRequiredService<IpcCallerBrio>()));
|
||||
collection.AddSingleton((s) => new NotificationService(s.GetRequiredService<ILogger<NotificationService>>(),
|
||||
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<DalamudUtilService>(),
|
||||
notificationManager, chatGui, s.GetRequiredService<LightlessConfigService>()));
|
||||
collection.AddSingleton((s) => new NotificationService(
|
||||
s.GetRequiredService<ILogger<NotificationService>>(),
|
||||
s.GetRequiredService<LightlessConfigService>(),
|
||||
s.GetRequiredService<DalamudUtilService>(),
|
||||
notificationManager,
|
||||
chatGui,
|
||||
s.GetRequiredService<LightlessMediator>(),
|
||||
s.GetRequiredService<PairRequestService>()));
|
||||
collection.AddSingleton((s) =>
|
||||
{
|
||||
var httpClient = new HttpClient();
|
||||
@@ -178,10 +198,12 @@ public sealed class Plugin : IDalamudPlugin
|
||||
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("LightlessSync", ver!.Major + "." + ver!.Minor + "." + ver!.Build));
|
||||
return httpClient;
|
||||
});
|
||||
collection.AddSingleton((s) => new UiThemeConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton((s) =>
|
||||
{
|
||||
var cfg = new LightlessConfigService(pluginInterface.ConfigDirectory.FullName);
|
||||
LightlessSync.UI.Style.MainStyle.Init(cfg);
|
||||
var theme = s.GetRequiredService<UiThemeConfigService>();
|
||||
LightlessSync.UI.Style.MainStyle.Init(cfg, theme);
|
||||
return cfg;
|
||||
});
|
||||
collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
@@ -193,6 +215,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
collection.AddSingleton((s) => new PlayerPerformanceConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton((s) => new CharaDataConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<LightlessConfigService>());
|
||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<UiThemeConfigService>());
|
||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<ServerConfigService>());
|
||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<NotesConfigService>());
|
||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<PairTagConfigService>());
|
||||
@@ -204,7 +227,6 @@ public sealed class Plugin : IDalamudPlugin
|
||||
collection.AddSingleton<ConfigurationMigrator>();
|
||||
collection.AddSingleton<ConfigurationSaveService>();
|
||||
collection.AddSingleton<HubFactory>();
|
||||
collection.AddSingleton<NameplateHandler>();
|
||||
collection.AddSingleton(s => new BroadcastScannerService( s.GetRequiredService<ILogger<BroadcastScannerService>>(), clientState, objectTable, framework, s.GetRequiredService<BroadcastService>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<NameplateHandler>(), s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<LightlessConfigService>()));
|
||||
|
||||
|
||||
@@ -230,8 +252,14 @@ public sealed class Plugin : IDalamudPlugin
|
||||
s.GetRequiredService<LightlessProfileManager>(), s.GetRequiredService<PerformanceCollectorService>()));
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, PopupHandler>();
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, BroadcastUI>((s) => new BroadcastUI(s.GetRequiredService<ILogger<BroadcastUI>>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(), s.GetRequiredService<BroadcastService>(), s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<BroadcastScannerService>()));
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, SyncshellFinderUI>((s) => new SyncshellFinderUI(s.GetRequiredService<ILogger<SyncshellFinderUI>>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(), s.GetRequiredService<BroadcastService>(), s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<BroadcastScannerService>()));
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, SyncshellFinderUI>((s) => new SyncshellFinderUI(s.GetRequiredService<ILogger<SyncshellFinderUI>>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(), s.GetRequiredService<BroadcastService>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<BroadcastScannerService>(), s.GetRequiredService<PairManager>(), s.GetRequiredService<DalamudUtilService>()));
|
||||
collection.AddScoped<IPopupHandler, BanUserPopupHandler>();
|
||||
collection.AddScoped<WindowMediatorSubscriberBase, LightlessNotificationUI>((s) =>
|
||||
new LightlessNotificationUI(
|
||||
s.GetRequiredService<ILogger<LightlessNotificationUI>>(),
|
||||
s.GetRequiredService<LightlessMediator>(),
|
||||
s.GetRequiredService<PerformanceCollectorService>(),
|
||||
s.GetRequiredService<LightlessConfigService>()));
|
||||
collection.AddScoped<IPopupHandler, CensusPopupHandler>();
|
||||
collection.AddScoped<CacheCreationService>();
|
||||
collection.AddScoped<PlayerDataFactory>();
|
||||
@@ -239,7 +267,9 @@ public sealed class Plugin : IDalamudPlugin
|
||||
collection.AddScoped((s) => new UiService(s.GetRequiredService<ILogger<UiService>>(), pluginInterface.UiBuilder, s.GetRequiredService<LightlessConfigService>(),
|
||||
s.GetRequiredService<WindowSystem>(), s.GetServices<WindowMediatorSubscriberBase>(),
|
||||
s.GetRequiredService<UiFactory>(),
|
||||
s.GetRequiredService<FileDialogManager>(), s.GetRequiredService<LightlessMediator>()));
|
||||
s.GetRequiredService<FileDialogManager>(),
|
||||
s.GetRequiredService<LightlessMediator>(),
|
||||
s.GetRequiredService<NotificationService>()));
|
||||
collection.AddScoped((s) => new CommandManagerService(commandManager, s.GetRequiredService<PerformanceCollectorService>(),
|
||||
s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<CacheMonitor>(), s.GetRequiredService<ApiController>(),
|
||||
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<LightlessConfigService>()));
|
||||
@@ -249,6 +279,8 @@ public sealed class Plugin : IDalamudPlugin
|
||||
s.GetRequiredService<LightlessMediator>()));
|
||||
collection.AddScoped((s) => new NameplateService(s.GetRequiredService<ILogger<NameplateService>>(), s.GetRequiredService<LightlessConfigService>(), namePlateGui, clientState,
|
||||
s.GetRequiredService<PairManager>(), s.GetRequiredService<LightlessMediator>()));
|
||||
collection.AddScoped((s) => new NameplateHandler(s.GetRequiredService<ILogger<NameplateHandler>>(), addonLifecycle, gameGui, s.GetRequiredService<DalamudUtilService>(),
|
||||
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), clientState, s.GetRequiredService<PairManager>()));
|
||||
|
||||
collection.AddHostedService(p => p.GetRequiredService<ConfigurationSaveService>());
|
||||
collection.AddHostedService(p => p.GetRequiredService<LightlessMediator>());
|
||||
@@ -261,7 +293,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
collection.AddHostedService(p => p.GetRequiredService<EventAggregator>());
|
||||
collection.AddHostedService(p => p.GetRequiredService<IpcProvider>());
|
||||
collection.AddHostedService(p => p.GetRequiredService<LightlessPlugin>());
|
||||
collection.AddHostedService(p => p.GetRequiredService<ContextMenu>());
|
||||
collection.AddHostedService(p => p.GetRequiredService<ContextMenuService>());
|
||||
collection.AddHostedService(p => p.GetRequiredService<BroadcastService>());
|
||||
})
|
||||
.Build();
|
||||
@@ -274,4 +306,4 @@ public sealed class Plugin : IDalamudPlugin
|
||||
_host.StopAsync().GetAwaiter().GetResult();
|
||||
_host.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,6 +211,16 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos
|
||||
UpdateSyncshellBroadcasts();
|
||||
}
|
||||
|
||||
public int CountActiveBroadcasts(string? excludeHashedCid = null)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var comparer = StringComparer.Ordinal;
|
||||
return _broadcastCache.Count(entry =>
|
||||
entry.Value.IsBroadcasting &&
|
||||
entry.Value.ExpiryTime > now &&
|
||||
(excludeHashedCid is null || !comparer.Equals(entry.Key, excludeHashedCid)));
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
@@ -4,11 +4,10 @@ using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.WebAPI;
|
||||
using LightlessSync.WebAPI.SignalR;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Threading;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
public class BroadcastService : IHostedService, IMediatorSubscriber
|
||||
@@ -16,12 +15,13 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
||||
private readonly ILogger<BroadcastService> _logger;
|
||||
private readonly ApiController _apiController;
|
||||
private readonly LightlessMediator _mediator;
|
||||
private readonly HubFactory _hubFactory;
|
||||
private readonly LightlessConfigService _config;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private CancellationTokenSource? _lightfinderCancelTokens;
|
||||
private Action? _connectedHandler;
|
||||
public LightlessMediator Mediator => _mediator;
|
||||
|
||||
public bool IsLightFinderAvailable { get; private set; } = true;
|
||||
public bool IsLightFinderAvailable { get; private set; } = false;
|
||||
|
||||
public bool IsBroadcasting => _config.Current.BroadcastEnabled;
|
||||
private bool _syncedOnStartup = false;
|
||||
@@ -29,53 +29,156 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
||||
private TimeSpan? _remainingTtl = null;
|
||||
private DateTime _lastTtlCheck = DateTime.MinValue;
|
||||
private DateTime _lastForcedDisableTime = DateTime.MinValue;
|
||||
private static readonly TimeSpan DisableCooldown = TimeSpan.FromSeconds(5);
|
||||
private static readonly TimeSpan _disableCooldown = TimeSpan.FromSeconds(5);
|
||||
public TimeSpan? RemainingTtl => _remainingTtl;
|
||||
public TimeSpan? RemainingCooldown
|
||||
{
|
||||
get
|
||||
{
|
||||
var elapsed = DateTime.UtcNow - _lastForcedDisableTime;
|
||||
if (elapsed >= DisableCooldown) return null;
|
||||
return DisableCooldown - elapsed;
|
||||
if (elapsed >= _disableCooldown) return null;
|
||||
return _disableCooldown - elapsed;
|
||||
}
|
||||
}
|
||||
public BroadcastService(ILogger<BroadcastService> logger, LightlessMediator mediator, HubFactory hubFactory, LightlessConfigService config, DalamudUtilService dalamudUtil, ApiController apiController)
|
||||
|
||||
public BroadcastService(ILogger<BroadcastService> logger, LightlessMediator mediator, LightlessConfigService config, DalamudUtilService dalamudUtil, ApiController apiController)
|
||||
{
|
||||
_logger = logger;
|
||||
_mediator = mediator;
|
||||
_hubFactory = hubFactory;
|
||||
_config = config;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_apiController = apiController;
|
||||
}
|
||||
|
||||
private async Task RequireConnectionAsync(string context, Func<Task> action)
|
||||
{
|
||||
if (!_apiController.IsConnected)
|
||||
{
|
||||
_logger.LogDebug($"{context} skipped, not connected");
|
||||
_logger.LogDebug(context + " skipped, not connected");
|
||||
return;
|
||||
}
|
||||
await action().ConfigureAwait(false);
|
||||
}
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
|
||||
private async Task<string?> GetLocalHashedCidAsync(string context)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cid = await _dalamudUtil.GetCIDAsync().ConfigureAwait(false);
|
||||
return cid.ToString().GetHash256();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to resolve CID for {Context}", context);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyBroadcastDisabled(bool forcePublish = false)
|
||||
{
|
||||
bool wasEnabled = _config.Current.BroadcastEnabled;
|
||||
bool hadExpiry = _config.Current.BroadcastTtl != DateTime.MinValue;
|
||||
bool hadRemaining = _remainingTtl.HasValue;
|
||||
|
||||
_config.Current.BroadcastEnabled = false;
|
||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||
|
||||
if (wasEnabled || hadExpiry)
|
||||
_config.Save();
|
||||
|
||||
_remainingTtl = null;
|
||||
_waitingForTtlFetch = false;
|
||||
_syncedOnStartup = false;
|
||||
|
||||
if (forcePublish || wasEnabled || hadRemaining)
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||
}
|
||||
|
||||
private bool TryApplyBroadcastEnabled(TimeSpan? ttl, string context)
|
||||
{
|
||||
if (ttl is not { } validTtl || validTtl <= TimeSpan.Zero)
|
||||
{
|
||||
_logger.LogWarning("Lightfinder enable skipped ({Context}): invalid TTL ({TTL})", context, ttl);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool wasEnabled = _config.Current.BroadcastEnabled;
|
||||
TimeSpan? previousRemaining = _remainingTtl;
|
||||
DateTime previousExpiry = _config.Current.BroadcastTtl;
|
||||
|
||||
var newExpiry = DateTime.UtcNow + validTtl;
|
||||
|
||||
_config.Current.BroadcastEnabled = true;
|
||||
_config.Current.BroadcastTtl = newExpiry;
|
||||
|
||||
if (!wasEnabled || previousExpiry != newExpiry)
|
||||
_config.Save();
|
||||
|
||||
_remainingTtl = validTtl;
|
||||
_waitingForTtlFetch = false;
|
||||
|
||||
if (!wasEnabled || previousRemaining != validTtl)
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(true, validTtl));
|
||||
|
||||
_logger.LogInformation("Lightfinder broadcast enabled ({Context}), TTL: {TTL}", context, validTtl);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void HandleLightfinderUnavailable(string message, Exception? ex = null)
|
||||
{
|
||||
if (ex != null)
|
||||
_logger.LogWarning(ex, message);
|
||||
else
|
||||
_logger.LogWarning(message);
|
||||
|
||||
IsLightFinderAvailable = false;
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
}
|
||||
|
||||
private void OnDisconnected()
|
||||
{
|
||||
IsLightFinderAvailable = false;
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
_logger.LogDebug("Cleared Lightfinder state due to disconnect.");
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_mediator.Subscribe<EnableBroadcastMessage>(this, OnEnableBroadcast);
|
||||
_mediator.Subscribe<BroadcastStatusChangedMessage>(this, OnBroadcastStatusChanged);
|
||||
_mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, OnTick);
|
||||
_mediator.Subscribe<DisconnectedMessage>(this, _ => OnDisconnected());
|
||||
|
||||
_apiController.OnConnected += () => _ = CheckLightfinderSupportAsync(cancellationToken);
|
||||
_ = CheckLightfinderSupportAsync(cancellationToken);
|
||||
IsLightFinderAvailable = false;
|
||||
|
||||
_lightfinderCancelTokens?.Cancel();
|
||||
_lightfinderCancelTokens?.Dispose();
|
||||
_lightfinderCancelTokens = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_connectedHandler = () => _ = CheckLightfinderSupportAsync(_lightfinderCancelTokens.Token);
|
||||
_apiController.OnConnected += _connectedHandler;
|
||||
|
||||
if (_apiController.IsConnected)
|
||||
_ = CheckLightfinderSupportAsync(_lightfinderCancelTokens.Token);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_lightfinderCancelTokens?.Cancel();
|
||||
_lightfinderCancelTokens?.Dispose();
|
||||
_lightfinderCancelTokens = null;
|
||||
|
||||
if (_connectedHandler is not null)
|
||||
{
|
||||
_apiController.OnConnected -= _connectedHandler;
|
||||
_connectedHandler = null;
|
||||
}
|
||||
|
||||
_mediator.UnsubscribeAll(this);
|
||||
_apiController.OnConnected -= () => _ = CheckLightfinderSupportAsync(cancellationToken);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// need to rework this, this is cooked
|
||||
private async Task CheckLightfinderSupportAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
@@ -86,26 +189,54 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
var hub = _hubFactory.GetOrCreate(CancellationToken.None);
|
||||
var dummy = "0".PadLeft(64, '0');
|
||||
var hashedCid = await GetLocalHashedCidAsync("Lightfinder state check").ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(hashedCid))
|
||||
return;
|
||||
|
||||
await hub.InvokeAsync<BroadcastStatusInfoDto?>("IsUserBroadcasting", dummy, cancellationToken);
|
||||
await hub.InvokeAsync("SetBroadcastStatus", dummy, true, null, cancellationToken);
|
||||
await hub.InvokeAsync<TimeSpan?>("GetBroadcastTtl", dummy, cancellationToken);
|
||||
await hub.InvokeAsync<Dictionary<string, BroadcastStatusInfoDto?>>("AreUsersBroadcasting", new[] { dummy }, cancellationToken);
|
||||
BroadcastStatusInfoDto? status = null;
|
||||
try
|
||||
{
|
||||
status = await _apiController.IsUserBroadcasting(hashedCid).ConfigureAwait(false);
|
||||
}
|
||||
catch (HubException ex) when (ex.Message.Contains("Method does not exist", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
HandleLightfinderUnavailable("Lightfinder unavailable on server (required method missing).", ex);
|
||||
}
|
||||
|
||||
if (!IsLightFinderAvailable)
|
||||
_logger.LogInformation("Lightfinder is available.");
|
||||
|
||||
IsLightFinderAvailable = true;
|
||||
_logger.LogInformation("Lightfinder is available.");
|
||||
}
|
||||
catch (HubException ex) when (ex.Message.Contains("Method does not exist"))
|
||||
{
|
||||
_logger.LogWarning("Lightfinder unavailable: required method missing.");
|
||||
IsLightFinderAvailable = false;
|
||||
|
||||
_config.Current.BroadcastEnabled = false;
|
||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||
_config.Save();
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||
bool isBroadcasting = status?.IsBroadcasting == true;
|
||||
TimeSpan? ttl = status?.TTL;
|
||||
|
||||
if (isBroadcasting)
|
||||
{
|
||||
if (ttl is not { } remaining || remaining <= TimeSpan.Zero)
|
||||
ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false);
|
||||
|
||||
if (TryApplyBroadcastEnabled(ttl, "server handshake"))
|
||||
{
|
||||
_syncedOnStartup = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
isBroadcasting = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isBroadcasting)
|
||||
{
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
_logger.LogInformation("Lightfinder is available but no active broadcast was found.");
|
||||
}
|
||||
|
||||
if (_config.Current.LightfinderAutoEnableOnConnect && !isBroadcasting)
|
||||
{
|
||||
_logger.LogInformation("Auto-enabling Lightfinder broadcast after reconnect.");
|
||||
_mediator.Publish(new EnableBroadcastMessage(hashedCid, true));
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -113,13 +244,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Lightfinder check failed.");
|
||||
IsLightFinderAvailable = false;
|
||||
|
||||
_config.Current.BroadcastEnabled = false;
|
||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||
_config.Save();
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||
HandleLightfinderUnavailable("Lightfinder check failed.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,46 +265,38 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
||||
};
|
||||
}
|
||||
|
||||
await _apiController.SetBroadcastStatus(msg.HashedCid, msg.Enabled, groupDto).ConfigureAwait(false);
|
||||
await _apiController.SetBroadcastStatus(msg.Enabled, groupDto).ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation("Broadcast {Status} for {Cid}", msg.Enabled ? "enabled" : "disabled", msg.HashedCid);
|
||||
_logger.LogDebug("Broadcast {Status} for {Cid}", msg.Enabled ? "enabled" : "disabled", msg.HashedCid);
|
||||
|
||||
if (!msg.Enabled)
|
||||
{
|
||||
_config.Current.BroadcastEnabled = false;
|
||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||
_config.Save();
|
||||
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational,$"Disabled Lightfinder for Player: {msg.HashedCid}")));
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Disabled Lightfinder for Player: {msg.HashedCid}")));
|
||||
return;
|
||||
}
|
||||
|
||||
_waitingForTtlFetch = true;
|
||||
|
||||
var ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false);
|
||||
|
||||
if (ttl is { } remaining && remaining > TimeSpan.Zero)
|
||||
try
|
||||
{
|
||||
_config.Current.BroadcastTtl = DateTime.UtcNow + remaining;
|
||||
_config.Current.BroadcastEnabled = true;
|
||||
_config.Save();
|
||||
TimeSpan? ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation("Fetched TTL from server: {TTL}", remaining);
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(true, remaining));
|
||||
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Enabled Lightfinder for Player: {msg.HashedCid}")));
|
||||
if (TryApplyBroadcastEnabled(ttl, "client request"))
|
||||
{
|
||||
_logger.LogDebug("Fetched TTL from server: {TTL}", ttl);
|
||||
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Enabled Lightfinder for Player: {msg.HashedCid}")));
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
_logger.LogWarning("No valid TTL returned after enabling broadcast. Disabling.");
|
||||
}
|
||||
}
|
||||
else
|
||||
finally
|
||||
{
|
||||
_logger.LogWarning("No valid TTL returned after enabling broadcast. Disabling.");
|
||||
_config.Current.BroadcastEnabled = false;
|
||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||
_config.Save();
|
||||
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||
_waitingForTtlFetch = false;
|
||||
}
|
||||
|
||||
_waitingForTtlFetch = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -202,13 +319,13 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("[BroadcastCheck] Checking CID: {cid}", targetCid);
|
||||
_logger.LogDebug("[BroadcastCheck] Checking CID: {cid}", targetCid);
|
||||
|
||||
var info = await _apiController.IsUserBroadcasting(targetCid).ConfigureAwait(false);
|
||||
result = info?.TTL > TimeSpan.Zero;
|
||||
|
||||
|
||||
_logger.LogInformation("[BroadcastCheck] Result for {cid}: {result} (TTL: {ttl}, GID: {gid})", targetCid, result, info?.TTL, info?.GID);
|
||||
_logger.LogDebug("[BroadcastCheck] Result for {cid}: {result} (TTL: {ttl}, GID: {gid})", targetCid, result, info?.TTL, info?.GID);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -220,17 +337,24 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<TimeSpan?> GetBroadcastTtlAsync(string cid)
|
||||
public async Task<TimeSpan?> GetBroadcastTtlAsync(string? cidForLog = null)
|
||||
{
|
||||
TimeSpan? ttl = null;
|
||||
await RequireConnectionAsync(nameof(GetBroadcastTtlAsync), async () => {
|
||||
try
|
||||
{
|
||||
ttl = await _apiController.GetBroadcastTtl(cid).ConfigureAwait(false);
|
||||
ttl = await _apiController.GetBroadcastTtl().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to fetch broadcast TTL for {cid}", cid);
|
||||
if (cidForLog is { Length: > 0 })
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to fetch broadcast TTL for {Cid}", cidForLog);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to fetch broadcast TTL");
|
||||
}
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
return ttl;
|
||||
@@ -252,7 +376,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
||||
result[kv.Key] = kv.Value;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Batch broadcast status check complete for {Count} CIDs", hashedCids.Count);
|
||||
_logger.LogTrace("Batch broadcast status check complete for {Count} CIDs", hashedCids.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -282,7 +406,12 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
||||
return;
|
||||
}
|
||||
|
||||
var hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
|
||||
var hashedCid = await GetLocalHashedCidAsync(nameof(ToggleBroadcast)).ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(hashedCid))
|
||||
{
|
||||
_logger.LogWarning("ToggleBroadcast - unable to resolve CID.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
@@ -292,10 +421,10 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
||||
if (!newStatus)
|
||||
{
|
||||
_lastForcedDisableTime = DateTime.UtcNow;
|
||||
_logger.LogInformation("Manual disable: cooldown timer started.");
|
||||
_logger.LogDebug("Manual disable: cooldown timer started.");
|
||||
}
|
||||
|
||||
_logger.LogInformation("Toggling broadcast. Server currently broadcasting: {ServerStatus}, setting to: {NewStatus}", isCurrentlyBroadcasting, newStatus);
|
||||
_logger.LogDebug("Toggling broadcast. Server currently broadcasting: {ServerStatus}, setting to: {NewStatus}", isCurrentlyBroadcasting, newStatus);
|
||||
|
||||
_mediator.Publish(new EnableBroadcastMessage(hashedCid, newStatus));
|
||||
}
|
||||
@@ -322,31 +451,31 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
||||
await RequireConnectionAsync(nameof(OnTick), async () => {
|
||||
if (!_syncedOnStartup && _config.Current.BroadcastEnabled)
|
||||
{
|
||||
_syncedOnStartup = true;
|
||||
try
|
||||
{
|
||||
var hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
|
||||
var ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false);
|
||||
if (ttl is { }
|
||||
remaining && remaining > TimeSpan.Zero)
|
||||
var hashedCid = await GetLocalHashedCidAsync("startup TTL refresh").ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(hashedCid))
|
||||
{
|
||||
_config.Current.BroadcastTtl = DateTime.UtcNow + remaining;
|
||||
_config.Current.BroadcastEnabled = true;
|
||||
_config.Save();
|
||||
_logger.LogInformation("Refreshed broadcast TTL from server on first OnTick: {TTL}", remaining);
|
||||
_logger.LogDebug("Skipping TTL refresh; hashed CID unavailable.");
|
||||
return;
|
||||
}
|
||||
|
||||
TimeSpan? ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false);
|
||||
if (TryApplyBroadcastEnabled(ttl, "startup TTL refresh"))
|
||||
{
|
||||
_syncedOnStartup = true;
|
||||
_logger.LogDebug("Refreshed broadcast TTL from server on first OnTick: {TTL}", ttl);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("No valid TTL found on OnTick. Disabling broadcast state.");
|
||||
_config.Current.BroadcastEnabled = false;
|
||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||
_config.Save();
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to refresh TTL in OnTick");
|
||||
_syncedOnStartup = false;
|
||||
}
|
||||
}
|
||||
if (_config.Current.BroadcastEnabled)
|
||||
@@ -357,16 +486,13 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
||||
return;
|
||||
}
|
||||
|
||||
var expiry = _config.Current.BroadcastTtl;
|
||||
var remaining = expiry - DateTime.UtcNow;
|
||||
DateTime expiry = _config.Current.BroadcastTtl;
|
||||
TimeSpan remaining = expiry - DateTime.UtcNow;
|
||||
_remainingTtl = remaining > TimeSpan.Zero ? remaining : null;
|
||||
if (_remainingTtl == null)
|
||||
{
|
||||
_logger.LogInformation("Broadcast TTL expired. Disabling broadcast locally.");
|
||||
_config.Current.BroadcastEnabled = false;
|
||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||
_config.Save();
|
||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||
_logger.LogDebug("Broadcast TTL expired. Disabling broadcast locally.");
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -42,7 +42,8 @@ public sealed class CommandManagerService : IDisposable
|
||||
"\t /light toggle on|off - Connects or disconnects to Lightless respectively" + Environment.NewLine +
|
||||
"\t /light gpose - Opens the Lightless Character Data Hub window" + Environment.NewLine +
|
||||
"\t /light analyze - Opens the Lightless Character Data Analysis window" + Environment.NewLine +
|
||||
"\t /light settings - Opens the Lightless Settings window"
|
||||
"\t /light settings - Opens the Lightless Settings window" + Environment.NewLine +
|
||||
"\t /light finder - Opens the Lightfinder window"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -122,5 +123,9 @@ public sealed class CommandManagerService : IDisposable
|
||||
{
|
||||
_mediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
|
||||
}
|
||||
else if (string.Equals(splitArgs[0], "finder", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_mediator.Publish(new UiToggleMessage(typeof(BroadcastUI)));
|
||||
}
|
||||
}
|
||||
}
|
||||
210
LightlessSync/Services/ContextMenuService.cs
Normal file
210
LightlessSync/Services/ContextMenuService.cs
Normal file
@@ -0,0 +1,210 @@
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.Gui.ContextMenu;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.WebAPI;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
|
||||
internal class ContextMenuService : IHostedService
|
||||
{
|
||||
private readonly IContextMenu _contextMenu;
|
||||
private readonly IDalamudPluginInterface _pluginInterface;
|
||||
private readonly IDataManager _gameData;
|
||||
private readonly ILogger<ContextMenuService> _logger;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly IClientState _clientState;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly PairRequestService _pairRequestService;
|
||||
private readonly ApiController _apiController;
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly LightlessConfigService _configService;
|
||||
|
||||
public ContextMenuService(
|
||||
IContextMenu contextMenu,
|
||||
IDalamudPluginInterface pluginInterface,
|
||||
IDataManager gameData,
|
||||
ILogger<ContextMenuService> logger,
|
||||
DalamudUtilService dalamudUtil,
|
||||
ApiController apiController,
|
||||
IObjectTable objectTable,
|
||||
LightlessConfigService configService,
|
||||
PairRequestService pairRequestService,
|
||||
PairManager pairManager,
|
||||
IClientState clientState)
|
||||
{
|
||||
_contextMenu = contextMenu;
|
||||
_pluginInterface = pluginInterface;
|
||||
_gameData = gameData;
|
||||
_logger = logger;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_apiController = apiController;
|
||||
_objectTable = objectTable;
|
||||
_configService = configService;
|
||||
_pairManager = pairManager;
|
||||
_pairRequestService = pairRequestService;
|
||||
_clientState = clientState;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_contextMenu.OnMenuOpened += OnMenuOpened;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_contextMenu.OnMenuOpened -= OnMenuOpened;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Enable()
|
||||
{
|
||||
_contextMenu.OnMenuOpened += OnMenuOpened;
|
||||
_logger.LogDebug("Context menu enabled.");
|
||||
}
|
||||
|
||||
public void Disable()
|
||||
{
|
||||
_contextMenu.OnMenuOpened -= OnMenuOpened;
|
||||
_logger.LogDebug("Context menu disabled.");
|
||||
}
|
||||
|
||||
private void OnMenuOpened(IMenuOpenedArgs args)
|
||||
{
|
||||
|
||||
if (!_pluginInterface.UiBuilder.ShouldModifyUi)
|
||||
return;
|
||||
|
||||
if (args.AddonName != null)
|
||||
return;
|
||||
|
||||
//Check if target is not menutargetdefault.
|
||||
if (args.Target is not MenuTargetDefault target)
|
||||
return;
|
||||
|
||||
//Check if name or target id isnt null/zero
|
||||
if (string.IsNullOrEmpty(target.TargetName) || target.TargetObjectId == 0 || target.TargetHomeWorld.RowId == 0)
|
||||
return;
|
||||
|
||||
//Check if it is a real target.
|
||||
IPlayerCharacter? targetData = GetPlayerFromObjectTable(target);
|
||||
if (targetData == null || targetData.Address == nint.Zero)
|
||||
return;
|
||||
|
||||
//Check if user is paired or is own.
|
||||
if (VisibleUserIds.Any(u => u == target.TargetObjectId) || _clientState.LocalPlayer.GameObjectId == target.TargetObjectId)
|
||||
return;
|
||||
|
||||
//Check if in PVP or GPose
|
||||
if (_clientState.IsPvPExcludingDen || _clientState.IsGPosing)
|
||||
return;
|
||||
|
||||
//Check for valid world.
|
||||
var world = GetWorld(target.TargetHomeWorld.RowId);
|
||||
if (!IsWorldValid(world))
|
||||
return;
|
||||
|
||||
if (!_configService.Current.EnableRightClickMenus)
|
||||
return;
|
||||
|
||||
args.AddMenuItem(new MenuItem
|
||||
{
|
||||
Name = "Send Pair Request",
|
||||
PrefixChar = 'L',
|
||||
UseDefaultPrefix = false,
|
||||
PrefixColor = 708,
|
||||
OnClicked = async _ => await HandleSelection(args).ConfigureAwait(false)
|
||||
});
|
||||
}
|
||||
|
||||
private async Task HandleSelection(IMenuArgs args)
|
||||
{
|
||||
if (args.Target is not MenuTargetDefault target)
|
||||
return;
|
||||
|
||||
var world = GetWorld(target.TargetHomeWorld.RowId);
|
||||
if (!IsWorldValid(world))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
IPlayerCharacter? targetData = GetPlayerFromObjectTable(target);
|
||||
|
||||
if (targetData == null || targetData.Address == nint.Zero)
|
||||
{
|
||||
_logger.LogWarning("Target player {TargetName}@{World} not found in object table.", target.TargetName, world.Name);
|
||||
return;
|
||||
}
|
||||
|
||||
var senderCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
|
||||
var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address);
|
||||
|
||||
_logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid);
|
||||
await _apiController.TryPairWithContentId(receiverCid).ConfigureAwait(false);
|
||||
if (!string.IsNullOrWhiteSpace(receiverCid))
|
||||
{
|
||||
_pairRequestService.RemoveRequest(receiverCid);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error sending pair request.");
|
||||
}
|
||||
}
|
||||
|
||||
private HashSet<ulong> VisibleUserIds => [.. _pairManager.GetOnlineUserPairs()
|
||||
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
|
||||
.Select(u => (ulong)u.PlayerCharacterId)];
|
||||
|
||||
private IPlayerCharacter? GetPlayerFromObjectTable(MenuTargetDefault target)
|
||||
{
|
||||
return _objectTable
|
||||
.OfType<IPlayerCharacter>()
|
||||
.FirstOrDefault(p =>
|
||||
string.Equals(p.Name.TextValue, target.TargetName, StringComparison.OrdinalIgnoreCase) &&
|
||||
p.HomeWorld.RowId == target.TargetHomeWorld.RowId);
|
||||
}
|
||||
|
||||
private World GetWorld(uint worldId)
|
||||
{
|
||||
var sheet = _gameData.GetExcelSheet<World>()!;
|
||||
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 bool IsWorldValid(uint worldId) => IsWorldValid(GetWorld(worldId));
|
||||
|
||||
public static bool IsWorldValid(World world)
|
||||
{
|
||||
var name = world.Name.ToString();
|
||||
return !string.IsNullOrWhiteSpace(name) && char.IsUpper(name[0]);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
@@ -541,7 +541,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
curWaitTime += tick;
|
||||
Thread.Sleep(tick);
|
||||
}
|
||||
|
||||
Thread.Sleep(tick * 2);
|
||||
}
|
||||
|
||||
@@ -557,6 +556,18 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
return result;
|
||||
}
|
||||
|
||||
public string? GetWorldNameFromPlayerAddress(nint address)
|
||||
{
|
||||
if (address == nint.Zero) return null;
|
||||
|
||||
EnsureIsOnFramework();
|
||||
var playerCharacter = _objectTable.OfType<IPlayerCharacter>().FirstOrDefault(p => p.Address == address);
|
||||
if (playerCharacter == null) return null;
|
||||
|
||||
var worldId = (ushort)playerCharacter.HomeWorld.RowId;
|
||||
return WorldData.Value.TryGetValue(worldId, out var worldName) ? worldName : null;
|
||||
}
|
||||
|
||||
private unsafe void CheckCharacterForDrawing(nint address, string characterName)
|
||||
{
|
||||
var gameObj = (GameObject*)address;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Dto;
|
||||
using LightlessSync.API.Dto.CharaData;
|
||||
@@ -17,6 +17,7 @@ namespace LightlessSync.Services.Mediator;
|
||||
public record SwitchToIntroUiMessage : MessageBase;
|
||||
public record SwitchToMainUiMessage : MessageBase;
|
||||
public record OpenSettingsUiMessage : MessageBase;
|
||||
public record OpenLightfinderSettingsMessage : MessageBase;
|
||||
public record DalamudLoginMessage : MessageBase;
|
||||
public record DalamudLogoutMessage : MessageBase;
|
||||
public record PriorityFrameworkUpdateMessage : SameThreadMessage;
|
||||
@@ -53,6 +54,8 @@ public record NotificationMessage
|
||||
public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage;
|
||||
public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage;
|
||||
public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage;
|
||||
public record LightlessNotificationMessage(LightlessSync.UI.Models.LightlessNotification Notification) : MessageBase;
|
||||
public record LightlessNotificationDismissMessage(string NotificationId) : MessageBase;
|
||||
public record CharacterDataAnalyzedMessage : MessageBase;
|
||||
public record PenumbraStartRedrawMessage(IntPtr Address) : MessageBase;
|
||||
public record PenumbraEndRedrawMessage(IntPtr Address) : MessageBase;
|
||||
@@ -77,6 +80,7 @@ public record OpenCensusPopupMessage() : MessageBase;
|
||||
public record OpenSyncshellAdminPanel(GroupFullInfoDto GroupInfo) : MessageBase;
|
||||
public record OpenPermissionWindow(Pair Pair) : MessageBase;
|
||||
public record DownloadLimitChangedMessage() : SameThreadMessage;
|
||||
public record PairProcessingLimitChangedMessage : SameThreadMessage;
|
||||
public record CensusUpdateMessage(byte Gender, byte RaceId, byte TribeId) : MessageBase;
|
||||
public record TargetPairMessage(Pair Pair) : MessageBase;
|
||||
public record CombatStartMessage : MessageBase;
|
||||
@@ -100,6 +104,7 @@ public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase
|
||||
public record EnableBroadcastMessage(string HashedCid, bool Enabled) : MessageBase;
|
||||
public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl) : MessageBase;
|
||||
public record SyncshellBroadcastsUpdatedMessage : MessageBase;
|
||||
public record PairRequestsUpdatedMessage : MessageBase;
|
||||
public record VisibilityChange : MessageBase;
|
||||
#pragma warning restore S2094
|
||||
#pragma warning restore MA0048 // File name must match type name
|
||||
@@ -1,5 +1,4 @@
|
||||
using Dalamud.Interface.Windowing;
|
||||
using LightlessSync.UI.Style;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Services.Mediator;
|
||||
@@ -34,18 +33,6 @@ public abstract class WindowMediatorSubscriberBase : Window, IMediatorSubscriber
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public override void PreDraw()
|
||||
{
|
||||
base.PreDraw();
|
||||
MainStyle.PushStyle(); // internally checks ShouldUseTheme
|
||||
}
|
||||
|
||||
public override void PostDraw()
|
||||
{
|
||||
MainStyle.PopStyle(); // always attempts to pop if pushed
|
||||
base.PostDraw();
|
||||
}
|
||||
|
||||
public override void Draw()
|
||||
{
|
||||
_performanceCollectorService.LogPerformance(this, $"Draw", DrawInternal);
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
using Dalamud.Game.Addon.Lifecycle;
|
||||
using Dalamud.Game.Addon.Lifecycle;
|
||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.UtilsEnum.Enum;
|
||||
|
||||
// Created using https://github.com/PunishedPineapple/Distance as a reference, thank you!
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
|
||||
@@ -18,7 +25,10 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
private readonly ILogger<NameplateHandler> _logger;
|
||||
private readonly IAddonLifecycle _addonLifecycle;
|
||||
private readonly IGameGui _gameGui;
|
||||
private readonly IClientState _clientState;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly LightlessMediator _mediator;
|
||||
public LightlessMediator Mediator => _mediator;
|
||||
|
||||
@@ -26,18 +36,31 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
private bool _needsLabelRefresh = false;
|
||||
private AddonNamePlate* mpNameplateAddon = null;
|
||||
private readonly AtkTextNode*[] mTextNodes = new AtkTextNode*[AddonNamePlate.NumNamePlateObjects];
|
||||
private readonly int[] _cachedNameplateTextWidths = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
private readonly int[] _cachedNameplateTextHeights = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
private readonly int[] _cachedNameplateContainerHeights = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
private readonly int[] _cachedNameplateTextOffsets = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
|
||||
internal const uint mNameplateNodeIDBase = 0x7D99D500;
|
||||
private const string DefaultLabelText = "LightFinder";
|
||||
private const SeIconChar DefaultIcon = SeIconChar.Hyadelyn;
|
||||
private const int ContainerOffsetX = 50;
|
||||
private static readonly string DefaultIconGlyph = SeIconCharExtensions.ToIconString(DefaultIcon);
|
||||
|
||||
private volatile HashSet<string> _activeBroadcastingCids = new();
|
||||
private volatile HashSet<string> _activeBroadcastingCids = [];
|
||||
|
||||
public NameplateHandler(ILogger<NameplateHandler> logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, DalamudUtilService dalamudUtil, LightlessMediator mediator)
|
||||
public NameplateHandler(ILogger<NameplateHandler> logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, DalamudUtilService dalamudUtil, LightlessConfigService configService, LightlessMediator mediator, IClientState clientState, PairManager pairManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_addonLifecycle = addonLifecycle;
|
||||
_gameGui = gameGui;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_configService = configService;
|
||||
_mediator = mediator;
|
||||
_clientState = clientState;
|
||||
_pairManager = pairManager;
|
||||
|
||||
System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
|
||||
}
|
||||
|
||||
internal void Init()
|
||||
@@ -96,6 +119,10 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
if (mpNameplateAddon != pNameplateAddon)
|
||||
{
|
||||
for (int i = 0; i < mTextNodes.Length; ++i) mTextNodes[i] = null;
|
||||
System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length);
|
||||
System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length);
|
||||
System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length);
|
||||
System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
|
||||
mpNameplateAddon = pNameplateAddon;
|
||||
if (mpNameplateAddon != null) CreateNameplateNodes();
|
||||
}
|
||||
@@ -156,6 +183,11 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length);
|
||||
System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length);
|
||||
System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length);
|
||||
System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
|
||||
}
|
||||
|
||||
private void HideAllNameplateNodes()
|
||||
@@ -177,6 +209,7 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
for (int i = 0; i < ui3DModule->NamePlateObjectInfoCount; ++i)
|
||||
{
|
||||
var objectInfo = ui3DModule->NamePlateObjectInfoPointers[i].Value;
|
||||
|
||||
if (objectInfo == null || objectInfo->GameObject == null)
|
||||
continue;
|
||||
|
||||
@@ -188,32 +221,217 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
if (pNode == null)
|
||||
continue;
|
||||
|
||||
if (mpNameplateAddon == null)
|
||||
continue;
|
||||
|
||||
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)objectInfo->GameObject);
|
||||
|
||||
//_logger.LogInformation($"checking cid: {cid}", cid);
|
||||
|
||||
if (cid == null || !_activeBroadcastingCids.Contains(cid))
|
||||
{
|
||||
pNode->AtkResNode.ToggleVisibility(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
pNode->AtkResNode.ToggleVisibility(true);
|
||||
if (!_configService.Current.LightfinderLabelShowOwn && (objectInfo->GameObject->GetGameObjectId() == _clientState.LocalPlayer.GameObjectId))
|
||||
{
|
||||
pNode->AtkResNode.ToggleVisibility(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_configService.Current.LightfinderLabelShowPaired && VisibleUserIds.Any(u => u == objectInfo->GameObject->GetGameObjectId()))
|
||||
{
|
||||
pNode->AtkResNode.ToggleVisibility(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
var nameplateObject = mpNameplateAddon->NamePlateObjectArray[nameplateIndex];
|
||||
nameplateObject.RootComponentNode->Component->UldManager.UpdateDrawNodeList();
|
||||
|
||||
var pNameplateIconNode = nameplateObject.MarkerIcon;
|
||||
var pNameplateResNode = nameplateObject.NameContainer;
|
||||
var pNameplateTextNode = nameplateObject.NameText;
|
||||
bool IsVisible = pNameplateIconNode->AtkResNode.IsVisible() || (pNameplateResNode->IsVisible() && pNameplateTextNode->AtkResNode.IsVisible()) || _configService.Current.LightfinderLabelShowHidden;
|
||||
pNode->AtkResNode.ToggleVisibility(IsVisible);
|
||||
|
||||
if (nameplateObject.RootComponentNode == null ||
|
||||
nameplateObject.NameContainer == null ||
|
||||
nameplateObject.NameText == null)
|
||||
{
|
||||
pNode->AtkResNode.ToggleVisibility(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
var nameContainer = nameplateObject.NameContainer;
|
||||
var nameText = nameplateObject.NameText;
|
||||
|
||||
var labelColor = UIColors.Get("LightlessPurple");
|
||||
var edgeColor = UIColors.Get("FullBlack");
|
||||
if (nameContainer == null || nameText == null)
|
||||
{
|
||||
pNode->AtkResNode.ToggleVisibility(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
var labelY = nameContainer->Height - nameplateObject.TextH - (int)(24 * nameText->AtkResNode.ScaleY);
|
||||
var labelColor = UIColors.Get("Lightfinder");
|
||||
var edgeColor = UIColors.Get("LightfinderEdge");
|
||||
var config = _configService.Current;
|
||||
|
||||
pNode->AtkResNode.SetPositionShort(58, (short)labelY);
|
||||
var scaleMultiplier = System.Math.Clamp(config.LightfinderLabelScale, 0.5f, 2.0f);
|
||||
var baseScale = config.LightfinderLabelUseIcon ? 1.0f : 0.5f;
|
||||
var effectiveScale = baseScale * scaleMultiplier;
|
||||
var labelContent = config.LightfinderLabelUseIcon
|
||||
? NormalizeIconGlyph(config.LightfinderLabelIconGlyph)
|
||||
: DefaultLabelText;
|
||||
|
||||
pNode->FontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed;
|
||||
pNode->AtkResNode.SetScale(effectiveScale, effectiveScale);
|
||||
var nodeWidth = (int)pNode->AtkResNode.GetWidth();
|
||||
if (nodeWidth <= 0)
|
||||
nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
||||
var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
|
||||
var baseFontSize = config.LightfinderLabelUseIcon ? 36f : 24f;
|
||||
var computedFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier);
|
||||
pNode->FontSize = (byte)System.Math.Clamp(computedFontSize, 1, 255);
|
||||
AlignmentType alignment;
|
||||
|
||||
var textScaleY = nameText->AtkResNode.ScaleY;
|
||||
if (textScaleY <= 0f)
|
||||
textScaleY = 1f;
|
||||
|
||||
var blockHeight = System.Math.Abs((int)nameplateObject.TextH);
|
||||
if (blockHeight > 0)
|
||||
{
|
||||
_cachedNameplateTextHeights[nameplateIndex] = blockHeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
blockHeight = _cachedNameplateTextHeights[nameplateIndex];
|
||||
}
|
||||
|
||||
if (blockHeight <= 0)
|
||||
{
|
||||
blockHeight = GetScaledTextHeight(nameText);
|
||||
if (blockHeight <= 0)
|
||||
blockHeight = nodeHeight;
|
||||
|
||||
_cachedNameplateTextHeights[nameplateIndex] = blockHeight;
|
||||
}
|
||||
|
||||
var containerHeight = (int)nameContainer->Height;
|
||||
if (containerHeight > 0)
|
||||
{
|
||||
_cachedNameplateContainerHeights[nameplateIndex] = containerHeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
containerHeight = _cachedNameplateContainerHeights[nameplateIndex];
|
||||
}
|
||||
|
||||
if (containerHeight <= 0)
|
||||
{
|
||||
containerHeight = blockHeight + (int)System.Math.Round(8 * textScaleY);
|
||||
if (containerHeight <= blockHeight)
|
||||
containerHeight = blockHeight + 1;
|
||||
|
||||
_cachedNameplateContainerHeights[nameplateIndex] = containerHeight;
|
||||
}
|
||||
|
||||
var blockTop = containerHeight - blockHeight;
|
||||
if (blockTop < 0)
|
||||
blockTop = 0;
|
||||
var verticalPadding = (int)System.Math.Round(4 * effectiveScale);
|
||||
|
||||
var positionY = blockTop - verticalPadding - nodeHeight;
|
||||
|
||||
var textWidth = System.Math.Abs((int)nameplateObject.TextW);
|
||||
if (textWidth <= 0)
|
||||
{
|
||||
textWidth = GetScaledTextWidth(nameText);
|
||||
if (textWidth <= 0)
|
||||
textWidth = nodeWidth;
|
||||
}
|
||||
|
||||
if (textWidth > 0)
|
||||
{
|
||||
_cachedNameplateTextWidths[nameplateIndex] = textWidth;
|
||||
}
|
||||
|
||||
var textOffset = (int)System.Math.Round(nameText->AtkResNode.X);
|
||||
var hasValidOffset = true;
|
||||
|
||||
if (System.Math.Abs((int)nameplateObject.TextW) > 0 || textOffset != 0)
|
||||
{
|
||||
_cachedNameplateTextOffsets[nameplateIndex] = textOffset;
|
||||
}
|
||||
else if (_cachedNameplateTextOffsets[nameplateIndex] != int.MinValue)
|
||||
{
|
||||
textOffset = _cachedNameplateTextOffsets[nameplateIndex];
|
||||
}
|
||||
else
|
||||
{
|
||||
hasValidOffset = false;
|
||||
}
|
||||
int positionX;
|
||||
|
||||
|
||||
if (!config.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal)))
|
||||
labelContent = DefaultLabelText;
|
||||
|
||||
pNode->FontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed;
|
||||
|
||||
pNode->SetText(labelContent);
|
||||
|
||||
if (!config.LightfinderLabelUseIcon)
|
||||
{
|
||||
pNode->TextFlags &= ~TextFlags.AutoAdjustNodeSize;
|
||||
pNode->AtkResNode.Width = 0;
|
||||
nodeWidth = (int)pNode->AtkResNode.GetWidth();
|
||||
if (nodeWidth <= 0)
|
||||
nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
||||
pNode->AtkResNode.Width = (ushort)nodeWidth;
|
||||
}
|
||||
else
|
||||
{
|
||||
pNode->TextFlags |= TextFlags.AutoAdjustNodeSize;
|
||||
pNode->AtkResNode.Width = 0;
|
||||
nodeWidth = pNode->AtkResNode.GetWidth();
|
||||
}
|
||||
|
||||
|
||||
if (config.LightfinderAutoAlign && nameContainer != null && hasValidOffset)
|
||||
{
|
||||
var nameplateWidth = (int)nameContainer->Width;
|
||||
|
||||
int leftPos = nameplateWidth / 8;
|
||||
int rightPos = nameplateWidth - nodeWidth - (nameplateWidth / 8);
|
||||
int centrePos = (nameplateWidth - nodeWidth) / 2;
|
||||
int staticMargin = 24;
|
||||
int calcMargin = (int)(nameplateWidth * 0.08f);
|
||||
|
||||
switch (config.LabelAlignment)
|
||||
{
|
||||
case LabelAlignment.Left:
|
||||
positionX = config.LightfinderLabelUseIcon ? leftPos + staticMargin : leftPos;
|
||||
alignment = AlignmentType.BottomLeft;
|
||||
break;
|
||||
case LabelAlignment.Right:
|
||||
positionX = config.LightfinderLabelUseIcon ? rightPos - staticMargin : nameplateWidth - nodeWidth + calcMargin;
|
||||
alignment = AlignmentType.BottomRight;
|
||||
break;
|
||||
default:
|
||||
positionX = config.LightfinderLabelUseIcon ? centrePos : centrePos + calcMargin;
|
||||
alignment = AlignmentType.Bottom;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
positionX = 58 + config.LightfinderLabelOffsetX;
|
||||
alignment = AlignmentType.Bottom;
|
||||
}
|
||||
|
||||
positionY += config.LightfinderLabelOffsetY;
|
||||
|
||||
alignment = (AlignmentType)System.Math.Clamp((int)alignment, 0, 8);
|
||||
pNode->AtkResNode.SetUseDepthBasedPriority(true);
|
||||
pNode->AtkResNode.SetScale(0.5f, 0.5f);
|
||||
|
||||
pNode->AtkResNode.Color.A = 255;
|
||||
|
||||
@@ -227,18 +445,98 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
pNode->EdgeColor.B = (byte)(edgeColor.Z * 255);
|
||||
pNode->EdgeColor.A = (byte)(edgeColor.W * 255);
|
||||
|
||||
pNode->FontSize = 24;
|
||||
pNode->AlignmentType = AlignmentType.Center;
|
||||
pNode->FontType = FontType.MiedingerMed;
|
||||
pNode->LineSpacing = 24;
|
||||
|
||||
if(!config.LightfinderLabelUseIcon)
|
||||
{
|
||||
pNode->AlignmentType = AlignmentType.Bottom;
|
||||
}
|
||||
else
|
||||
{
|
||||
pNode->AlignmentType = alignment;
|
||||
}
|
||||
pNode->AtkResNode.SetPositionShort(
|
||||
(short)System.Math.Clamp(positionX, short.MinValue, short.MaxValue),
|
||||
(short)System.Math.Clamp(positionY, short.MinValue, short.MaxValue)
|
||||
);
|
||||
var computedLineSpacing = (int)System.Math.Round(24 * scaleMultiplier);
|
||||
pNode->LineSpacing = (byte)System.Math.Clamp(computedLineSpacing, 0, byte.MaxValue);
|
||||
pNode->CharSpacing = 1;
|
||||
|
||||
pNode->TextFlags = TextFlags.Edge | TextFlags.Glare;
|
||||
|
||||
pNode->SetText("Lightfinder");
|
||||
pNode->TextFlags = config.LightfinderLabelUseIcon
|
||||
? TextFlags.Edge | TextFlags.Glare | TextFlags.AutoAdjustNodeSize
|
||||
: TextFlags.Edge | TextFlags.Glare;
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe int GetScaledTextHeight(AtkTextNode* node)
|
||||
{
|
||||
if (node == null)
|
||||
return 0;
|
||||
|
||||
var resNode = &node->AtkResNode;
|
||||
var rawHeight = (int)resNode->GetHeight();
|
||||
if (rawHeight <= 0 && node->LineSpacing > 0)
|
||||
rawHeight = node->LineSpacing;
|
||||
if (rawHeight <= 0)
|
||||
rawHeight = AtkNodeHelpers.DefaultTextNodeHeight;
|
||||
|
||||
var scale = resNode->ScaleY;
|
||||
if (scale <= 0f)
|
||||
scale = 1f;
|
||||
|
||||
var computed = (int)System.Math.Round(rawHeight * scale);
|
||||
return System.Math.Max(1, computed);
|
||||
}
|
||||
|
||||
private static unsafe int GetScaledTextWidth(AtkTextNode* node)
|
||||
{
|
||||
if (node == null)
|
||||
return 0;
|
||||
|
||||
var resNode = &node->AtkResNode;
|
||||
var rawWidth = (int)resNode->GetWidth();
|
||||
if (rawWidth <= 0)
|
||||
rawWidth = AtkNodeHelpers.DefaultTextNodeWidth;
|
||||
|
||||
var scale = resNode->ScaleX;
|
||||
if (scale <= 0f)
|
||||
scale = 1f;
|
||||
|
||||
var computed = (int)System.Math.Round(rawWidth * scale);
|
||||
return System.Math.Max(1, computed);
|
||||
}
|
||||
|
||||
internal static string NormalizeIconGlyph(string? rawInput)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawInput))
|
||||
return DefaultIconGlyph;
|
||||
|
||||
var trimmed = rawInput.Trim();
|
||||
|
||||
if (Enum.TryParse<SeIconChar>(trimmed, true, out var iconEnum))
|
||||
return SeIconCharExtensions.ToIconString(iconEnum);
|
||||
|
||||
var hexCandidate = trimmed.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
|
||||
? trimmed[2..]
|
||||
: trimmed;
|
||||
|
||||
if (ushort.TryParse(hexCandidate, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var hexValue))
|
||||
return char.ConvertFromUtf32(hexValue);
|
||||
|
||||
var enumerator = trimmed.EnumerateRunes();
|
||||
if (enumerator.MoveNext())
|
||||
return enumerator.Current.ToString();
|
||||
|
||||
return DefaultIconGlyph;
|
||||
}
|
||||
|
||||
internal static string ToIconEditorString(string? rawInput)
|
||||
{
|
||||
var normalized = NormalizeIconGlyph(rawInput);
|
||||
var runeEnumerator = normalized.EnumerateRunes();
|
||||
return runeEnumerator.MoveNext()
|
||||
? runeEnumerator.Current.Value.ToString("X4", CultureInfo.InvariantCulture)
|
||||
: DefaultIconGlyph;
|
||||
}
|
||||
private void HideNameplateTextNode(int i)
|
||||
{
|
||||
var pNode = mTextNodes[i];
|
||||
@@ -267,6 +565,9 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
var nameplateObject = GetNameplateObject(i);
|
||||
return nameplateObject != null ? nameplateObject.Value.RootComponentNode : null;
|
||||
}
|
||||
private HashSet<ulong> VisibleUserIds => [.. _pairManager.GetOnlineUserPairs()
|
||||
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
|
||||
.Select(u => (ulong)u.PlayerCharacterId)];
|
||||
|
||||
public void FlagRefresh()
|
||||
{
|
||||
@@ -298,4 +599,12 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
|
||||
FlagRefresh();
|
||||
}
|
||||
|
||||
public void ClearNameplateCaches()
|
||||
{
|
||||
System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length);
|
||||
System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length);
|
||||
System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length);
|
||||
System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.Gui.NamePlate;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Plugin.Services;
|
||||
@@ -35,12 +35,10 @@ public class NameplateService : DisposableMediatorSubscriberBase
|
||||
_namePlateGui.OnNamePlateUpdate += OnNamePlateUpdate;
|
||||
_namePlateGui.RequestRedraw();
|
||||
Mediator.Subscribe<VisibilityChange>(this, (_) => _namePlateGui.RequestRedraw());
|
||||
|
||||
}
|
||||
|
||||
private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
|
||||
{
|
||||
|
||||
if (!_configService.Current.IsNameplateColorsEnabled || (_configService.Current.IsNameplateColorsEnabled && _clientState.IsPvPExcludingDen))
|
||||
return;
|
||||
|
||||
@@ -69,16 +67,26 @@ public class NameplateService : DisposableMediatorSubscriberBase
|
||||
(isFriend && !friendColorAllowed)
|
||||
))
|
||||
{
|
||||
//_logger.LogInformation("added nameplate color to {Name}", playerCharacter.Name.TextValue);
|
||||
handler.NameParts.TextWrap = CreateTextWrap(colors);
|
||||
}
|
||||
|
||||
if (_configService.Current.overrideFcTagColor)
|
||||
{
|
||||
bool hasActualFcTag = playerCharacter.CompanyTag.TextValue.Length > 0;
|
||||
bool isFromDifferentRealm = playerCharacter.HomeWorld.RowId != playerCharacter.CurrentWorld.RowId;
|
||||
bool shouldColorFcArea = hasActualFcTag || (!hasActualFcTag && isFromDifferentRealm);
|
||||
|
||||
if (shouldColorFcArea)
|
||||
{
|
||||
handler.FreeCompanyTagParts.OuterWrap = CreateTextWrap(colors);
|
||||
handler.FreeCompanyTagParts.TextWrap = CreateTextWrap(colors);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RequestRedraw()
|
||||
{
|
||||
|
||||
_namePlateGui.RequestRedraw();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,62 +1,495 @@
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Dalamud.Plugin.Services;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI;
|
||||
using LightlessSync.UI.Models;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using NotificationType = LightlessSync.LightlessConfiguration.Models.NotificationType;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
|
||||
public class NotificationService : DisposableMediatorSubscriberBase, IHostedService
|
||||
{
|
||||
private readonly ILogger<NotificationService> _logger;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private readonly INotificationManager _notificationManager;
|
||||
private readonly IChatGui _chatGui;
|
||||
private readonly LightlessConfigService _configurationService;
|
||||
private readonly PairRequestService _pairRequestService;
|
||||
private readonly HashSet<string> _shownPairRequestNotifications = new();
|
||||
|
||||
public NotificationService(ILogger<NotificationService> logger, LightlessMediator mediator,
|
||||
public NotificationService(
|
||||
ILogger<NotificationService> logger,
|
||||
LightlessConfigService configService,
|
||||
DalamudUtilService dalamudUtilService,
|
||||
INotificationManager notificationManager,
|
||||
IChatGui chatGui, LightlessConfigService configurationService) : base(logger, mediator)
|
||||
IChatGui chatGui,
|
||||
LightlessMediator mediator,
|
||||
PairRequestService pairRequestService) : base(logger, mediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_configService = configService;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
_notificationManager = notificationManager;
|
||||
_chatGui = chatGui;
|
||||
_configurationService = configurationService;
|
||||
_pairRequestService = pairRequestService;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Mediator.Subscribe<NotificationMessage>(this, ShowNotification);
|
||||
Mediator.Subscribe<NotificationMessage>(this, HandleNotificationMessage);
|
||||
Mediator.Subscribe<PairRequestsUpdatedMessage>(this, HandlePairRequestsUpdated);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
public void ShowNotification(string title, string message, NotificationType type = NotificationType.Info,
|
||||
TimeSpan? duration = null, List<LightlessNotificationAction>? actions = null, uint? soundEffectId = null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
var notification = CreateNotification(title, message, type, duration, actions, soundEffectId);
|
||||
|
||||
if (_configService.Current.AutoDismissOnAction && notification.Actions.Any())
|
||||
{
|
||||
WrapActionsWithAutoDismiss(notification);
|
||||
}
|
||||
|
||||
if (notification.SoundEffectId.HasValue)
|
||||
{
|
||||
PlayNotificationSound(notification.SoundEffectId.Value);
|
||||
}
|
||||
|
||||
Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||
}
|
||||
|
||||
private void PrintErrorChat(string? message)
|
||||
private LightlessNotification CreateNotification(string title, string message, NotificationType type,
|
||||
TimeSpan? duration, List<LightlessNotificationAction>? actions, uint? soundEffectId)
|
||||
{
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Error: " + message);
|
||||
_chatGui.PrintError(se.BuiltString);
|
||||
return new LightlessNotification
|
||||
{
|
||||
Title = title,
|
||||
Message = message,
|
||||
Type = type,
|
||||
Duration = duration ?? GetDefaultDurationForType(type),
|
||||
Actions = actions ?? new List<LightlessNotificationAction>(),
|
||||
SoundEffectId = GetSoundEffectId(type, soundEffectId),
|
||||
ShowProgress = _configService.Current.ShowNotificationProgress,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
private void PrintInfoChat(string? message)
|
||||
private void WrapActionsWithAutoDismiss(LightlessNotification notification)
|
||||
{
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Info: ").AddItalics(message ?? string.Empty);
|
||||
_chatGui.Print(se.BuiltString);
|
||||
foreach (var action in notification.Actions)
|
||||
{
|
||||
var originalOnClick = action.OnClick;
|
||||
action.OnClick = (n) =>
|
||||
{
|
||||
originalOnClick(n);
|
||||
if (_configService.Current.AutoDismissOnAction)
|
||||
{
|
||||
DismissNotification(n);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private void PrintWarnChat(string? message)
|
||||
private void DismissNotification(LightlessNotification notification)
|
||||
{
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ").AddUiForeground("Warning: " + (message ?? string.Empty), 31).AddUiForegroundOff();
|
||||
_chatGui.Print(se.BuiltString);
|
||||
notification.IsDismissed = true;
|
||||
notification.IsAnimatingOut = true;
|
||||
}
|
||||
|
||||
public void ShowPairRequestNotification(string senderName, string senderId, Action onAccept, Action onDecline)
|
||||
{
|
||||
var notification = new LightlessNotification
|
||||
{
|
||||
Id = $"pair_request_{senderId}",
|
||||
Title = "Pair Request Received",
|
||||
Message = $"{senderName} wants to directly pair with you.",
|
||||
Type = NotificationType.PairRequest,
|
||||
Duration = TimeSpan.FromSeconds(_configService.Current.PairRequestDurationSeconds),
|
||||
SoundEffectId = GetPairRequestSoundId(),
|
||||
Actions = CreatePairRequestActions(onAccept, onDecline)
|
||||
};
|
||||
|
||||
if (notification.SoundEffectId.HasValue)
|
||||
{
|
||||
PlayNotificationSound(notification.SoundEffectId.Value);
|
||||
}
|
||||
|
||||
Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||
}
|
||||
|
||||
private uint? GetPairRequestSoundId() =>
|
||||
!_configService.Current.DisablePairRequestSound ? _configService.Current.PairRequestSoundId : null;
|
||||
|
||||
private List<LightlessNotificationAction> CreatePairRequestActions(Action onAccept, Action onDecline)
|
||||
{
|
||||
return new List<LightlessNotificationAction>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = "accept",
|
||||
Label = "Accept",
|
||||
Icon = FontAwesomeIcon.Check,
|
||||
Color = UIColors.Get("LightlessGreen"),
|
||||
IsPrimary = true,
|
||||
OnClick = (n) =>
|
||||
{
|
||||
_logger.LogInformation("Pair request accepted");
|
||||
onAccept();
|
||||
DismissNotification(n);
|
||||
}
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = "decline",
|
||||
Label = "Decline",
|
||||
Icon = FontAwesomeIcon.Times,
|
||||
Color = UIColors.Get("DimRed"),
|
||||
IsDestructive = true,
|
||||
OnClick = (n) =>
|
||||
{
|
||||
_logger.LogInformation("Pair request declined");
|
||||
onDecline();
|
||||
DismissNotification(n);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void ShowDownloadCompleteNotification(string fileName, int fileCount, Action? onOpenFolder = null)
|
||||
{
|
||||
var notification = new LightlessNotification
|
||||
{
|
||||
Title = "Download Complete",
|
||||
Message = FormatDownloadCompleteMessage(fileName, fileCount),
|
||||
Type = NotificationType.Info,
|
||||
Duration = TimeSpan.FromSeconds(8),
|
||||
Actions = CreateDownloadCompleteActions(onOpenFolder),
|
||||
SoundEffectId = NotificationSounds.DownloadComplete
|
||||
};
|
||||
|
||||
if (notification.SoundEffectId.HasValue)
|
||||
{
|
||||
PlayNotificationSound(notification.SoundEffectId.Value);
|
||||
}
|
||||
|
||||
Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||
}
|
||||
|
||||
private string FormatDownloadCompleteMessage(string fileName, int fileCount) =>
|
||||
fileCount > 1
|
||||
? $"Downloaded {fileCount} files successfully."
|
||||
: $"Downloaded {fileName} successfully.";
|
||||
|
||||
private List<LightlessNotificationAction> CreateDownloadCompleteActions(Action? onOpenFolder)
|
||||
{
|
||||
var actions = new List<LightlessNotificationAction>();
|
||||
|
||||
if (onOpenFolder != null)
|
||||
{
|
||||
actions.Add(new LightlessNotificationAction
|
||||
{
|
||||
Id = "open_folder",
|
||||
Label = "Open Folder",
|
||||
Icon = FontAwesomeIcon.FolderOpen,
|
||||
Color = UIColors.Get("LightlessBlue"),
|
||||
OnClick = (n) =>
|
||||
{
|
||||
onOpenFolder();
|
||||
DismissNotification(n);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
public void ShowErrorNotification(string title, string message, Exception? exception = null, Action? onRetry = null,
|
||||
Action? onViewLog = null)
|
||||
{
|
||||
var notification = new LightlessNotification
|
||||
{
|
||||
Title = title,
|
||||
Message = FormatErrorMessage(message, exception),
|
||||
Type = NotificationType.Error,
|
||||
Duration = TimeSpan.FromSeconds(15),
|
||||
Actions = CreateErrorActions(onRetry, onViewLog),
|
||||
SoundEffectId = NotificationSounds.Error
|
||||
};
|
||||
|
||||
if (notification.SoundEffectId.HasValue)
|
||||
{
|
||||
PlayNotificationSound(notification.SoundEffectId.Value);
|
||||
}
|
||||
|
||||
Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||
}
|
||||
|
||||
private string FormatErrorMessage(string message, Exception? exception) =>
|
||||
exception != null ? $"{message}\n\nError: {exception.Message}" : message;
|
||||
|
||||
private List<LightlessNotificationAction> CreateErrorActions(Action? onRetry, Action? onViewLog)
|
||||
{
|
||||
var actions = new List<LightlessNotificationAction>();
|
||||
|
||||
if (onRetry != null)
|
||||
{
|
||||
actions.Add(new LightlessNotificationAction
|
||||
{
|
||||
Id = "retry",
|
||||
Label = "Retry",
|
||||
Icon = FontAwesomeIcon.Redo,
|
||||
Color = UIColors.Get("LightlessBlue"),
|
||||
OnClick = (n) =>
|
||||
{
|
||||
onRetry();
|
||||
DismissNotification(n);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (onViewLog != null)
|
||||
{
|
||||
actions.Add(new LightlessNotificationAction
|
||||
{
|
||||
Id = "view_log",
|
||||
Label = "View Log",
|
||||
Icon = FontAwesomeIcon.FileAlt,
|
||||
Color = UIColors.Get("LightlessYellow"),
|
||||
OnClick = (n) => onViewLog()
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
public void ShowPairDownloadNotification(List<(string playerName, float progress, string status)> downloadStatus,
|
||||
int queueWaiting = 0)
|
||||
{
|
||||
var userDownloads = downloadStatus.Where(x => x.playerName != "Pair Queue").ToList();
|
||||
var totalProgress = userDownloads.Count > 0 ? userDownloads.Average(x => x.progress) : 0f;
|
||||
var message = BuildPairDownloadMessage(userDownloads, queueWaiting);
|
||||
|
||||
var notification = new LightlessNotification
|
||||
{
|
||||
Id = "pair_download_progress",
|
||||
Title = "Downloading Pair Data",
|
||||
Message = message,
|
||||
Type = NotificationType.Download,
|
||||
Duration = TimeSpan.FromSeconds(_configService.Current.DownloadNotificationDurationSeconds),
|
||||
ShowProgress = true,
|
||||
Progress = totalProgress
|
||||
};
|
||||
|
||||
Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||
|
||||
if (AreAllDownloadsCompleted(userDownloads))
|
||||
{
|
||||
DismissPairDownloadNotification();
|
||||
}
|
||||
}
|
||||
|
||||
private string BuildPairDownloadMessage(List<(string playerName, float progress, string status)> userDownloads,
|
||||
int queueWaiting)
|
||||
{
|
||||
var messageParts = new List<string>();
|
||||
|
||||
if (queueWaiting > 0)
|
||||
{
|
||||
messageParts.Add($"Queue: {queueWaiting} waiting");
|
||||
}
|
||||
|
||||
if (userDownloads.Count > 0)
|
||||
{
|
||||
var completedCount = userDownloads.Count(x => x.progress >= 1.0f);
|
||||
messageParts.Add($"Progress: {completedCount}/{userDownloads.Count} completed");
|
||||
}
|
||||
|
||||
var activeDownloadLines = BuildActiveDownloadLines(userDownloads);
|
||||
if (!string.IsNullOrEmpty(activeDownloadLines))
|
||||
{
|
||||
messageParts.Add(activeDownloadLines);
|
||||
}
|
||||
|
||||
return string.Join("\n", messageParts);
|
||||
}
|
||||
|
||||
private string BuildActiveDownloadLines(List<(string playerName, float progress, string status)> userDownloads)
|
||||
{
|
||||
var activeDownloads = userDownloads
|
||||
.Where(x => x.progress < 1.0f)
|
||||
.Take(_configService.Current.MaxConcurrentPairApplications);
|
||||
|
||||
if (!activeDownloads.Any()) return string.Empty;
|
||||
|
||||
return string.Join("\n", activeDownloads.Select(x => $"• {x.playerName}: {FormatDownloadStatus(x)}"));
|
||||
}
|
||||
|
||||
private string FormatDownloadStatus((string playerName, float progress, string status) download) =>
|
||||
download.status switch
|
||||
{
|
||||
"downloading" => $"{download.progress:P0}",
|
||||
"decompressing" => "decompressing",
|
||||
"queued" => "queued",
|
||||
"waiting" => "waiting for slot",
|
||||
_ => download.status
|
||||
};
|
||||
|
||||
private bool AreAllDownloadsCompleted(List<(string playerName, float progress, string status)> userDownloads) =>
|
||||
userDownloads.Any() && userDownloads.All(x => x.progress >= 1.0f);
|
||||
|
||||
public void DismissPairDownloadNotification() =>
|
||||
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
|
||||
|
||||
private TimeSpan GetDefaultDurationForType(NotificationType type) => type switch
|
||||
{
|
||||
NotificationType.Info => TimeSpan.FromSeconds(_configService.Current.InfoNotificationDurationSeconds),
|
||||
NotificationType.Warning => TimeSpan.FromSeconds(_configService.Current.WarningNotificationDurationSeconds),
|
||||
NotificationType.Error => TimeSpan.FromSeconds(_configService.Current.ErrorNotificationDurationSeconds),
|
||||
NotificationType.PairRequest => TimeSpan.FromSeconds(_configService.Current.PairRequestDurationSeconds),
|
||||
NotificationType.Download => TimeSpan.FromSeconds(_configService.Current.DownloadNotificationDurationSeconds),
|
||||
_ => TimeSpan.FromSeconds(10)
|
||||
};
|
||||
|
||||
private uint? GetSoundEffectId(NotificationType type, uint? overrideSoundId)
|
||||
{
|
||||
if (overrideSoundId.HasValue) return overrideSoundId;
|
||||
if (IsSoundDisabledForType(type)) return null;
|
||||
return GetConfiguredSoundForType(type);
|
||||
}
|
||||
|
||||
private bool IsSoundDisabledForType(NotificationType type) => type switch
|
||||
{
|
||||
NotificationType.Info => _configService.Current.DisableInfoSound,
|
||||
NotificationType.Warning => _configService.Current.DisableWarningSound,
|
||||
NotificationType.Error => _configService.Current.DisableErrorSound,
|
||||
NotificationType.Download => _configService.Current.DisableDownloadSound,
|
||||
_ => false
|
||||
};
|
||||
|
||||
private uint GetConfiguredSoundForType(NotificationType type) => type switch
|
||||
{
|
||||
NotificationType.Info => _configService.Current.CustomInfoSoundId,
|
||||
NotificationType.Warning => _configService.Current.CustomWarningSoundId,
|
||||
NotificationType.Error => _configService.Current.CustomErrorSoundId,
|
||||
NotificationType.Download => _configService.Current.DownloadSoundId,
|
||||
_ => NotificationSounds.GetDefaultSound(type)
|
||||
};
|
||||
|
||||
private void PlayNotificationSound(uint soundEffectId)
|
||||
{
|
||||
try
|
||||
{
|
||||
UIGlobals.PlayChatSoundEffect(soundEffectId);
|
||||
_logger.LogDebug("Played notification sound effect {SoundId} via ChatGui", soundEffectId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to play notification sound effect {SoundId}", soundEffectId);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleNotificationMessage(NotificationMessage msg)
|
||||
{
|
||||
_logger.LogInformation("{msg}", msg.ToString());
|
||||
if (!_dalamudUtilService.IsLoggedIn) return;
|
||||
|
||||
var location = GetNotificationLocation(msg.Type);
|
||||
ShowNotificationLocationBased(msg, location);
|
||||
}
|
||||
|
||||
private NotificationLocation GetNotificationLocation(NotificationType type) =>
|
||||
_configService.Current.UseLightlessNotifications
|
||||
? GetLightlessNotificationLocation(type)
|
||||
: GetClassicNotificationLocation(type);
|
||||
|
||||
private NotificationLocation GetLightlessNotificationLocation(NotificationType type) => type switch
|
||||
{
|
||||
NotificationType.Info => _configService.Current.LightlessInfoNotification,
|
||||
NotificationType.Warning => _configService.Current.LightlessWarningNotification,
|
||||
NotificationType.Error => _configService.Current.LightlessErrorNotification,
|
||||
NotificationType.PairRequest => _configService.Current.LightlessPairRequestNotification,
|
||||
NotificationType.Download => _configService.Current.LightlessDownloadNotification,
|
||||
_ => NotificationLocation.LightlessUi
|
||||
};
|
||||
|
||||
private NotificationLocation GetClassicNotificationLocation(NotificationType type) => type switch
|
||||
{
|
||||
NotificationType.Info => _configService.Current.InfoNotification,
|
||||
NotificationType.Warning => _configService.Current.WarningNotification,
|
||||
NotificationType.Error => _configService.Current.ErrorNotification,
|
||||
NotificationType.PairRequest => NotificationLocation.Toast,
|
||||
NotificationType.Download => NotificationLocation.Toast,
|
||||
_ => NotificationLocation.Nowhere
|
||||
};
|
||||
|
||||
private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location)
|
||||
{
|
||||
switch (location)
|
||||
{
|
||||
case NotificationLocation.Toast:
|
||||
ShowToast(msg);
|
||||
break;
|
||||
|
||||
case NotificationLocation.Chat:
|
||||
ShowChat(msg);
|
||||
break;
|
||||
|
||||
case NotificationLocation.Both:
|
||||
ShowToast(msg);
|
||||
ShowChat(msg);
|
||||
break;
|
||||
|
||||
case NotificationLocation.LightlessUi:
|
||||
ShowLightlessNotification(msg);
|
||||
break;
|
||||
|
||||
case NotificationLocation.ChatAndLightlessUi:
|
||||
ShowChat(msg);
|
||||
ShowLightlessNotification(msg);
|
||||
break;
|
||||
|
||||
case NotificationLocation.Nowhere:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowLightlessNotification(NotificationMessage msg)
|
||||
{
|
||||
var duration = msg.TimeShownOnScreen ?? GetDefaultDurationForType(msg.Type);
|
||||
ShowNotification(msg.Title ?? "Lightless Sync", msg.Message ?? string.Empty, msg.Type, duration, null, null);
|
||||
}
|
||||
|
||||
private void ShowToast(NotificationMessage msg)
|
||||
{
|
||||
var dalamudType = ConvertToDalamudNotificationType(msg.Type);
|
||||
|
||||
_notificationManager.AddNotification(new Notification()
|
||||
{
|
||||
Content = msg.Message ?? string.Empty,
|
||||
Title = msg.Title,
|
||||
Type = dalamudType,
|
||||
Minimized = false,
|
||||
InitialDuration = msg.TimeShownOnScreen ?? TimeSpan.FromSeconds(3)
|
||||
});
|
||||
}
|
||||
|
||||
private Dalamud.Interface.ImGuiNotification.NotificationType
|
||||
ConvertToDalamudNotificationType(NotificationType type) => type switch
|
||||
{
|
||||
NotificationType.Error => Dalamud.Interface.ImGuiNotification.NotificationType.Error,
|
||||
NotificationType.Warning => Dalamud.Interface.ImGuiNotification.NotificationType.Warning,
|
||||
_ => Dalamud.Interface.ImGuiNotification.NotificationType.Info
|
||||
};
|
||||
|
||||
private void ShowChat(NotificationMessage msg)
|
||||
{
|
||||
switch (msg.Type)
|
||||
@@ -75,67 +508,54 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowNotification(NotificationMessage msg)
|
||||
private void PrintErrorChat(string? message)
|
||||
{
|
||||
Logger.LogInformation("{msg}", msg.ToString());
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Error: " + message);
|
||||
_chatGui.PrintError(se.BuiltString);
|
||||
}
|
||||
|
||||
if (!_dalamudUtilService.IsLoggedIn) return;
|
||||
private void PrintInfoChat(string? message)
|
||||
{
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Info: ")
|
||||
.AddItalics(message ?? string.Empty);
|
||||
_chatGui.Print(se.BuiltString);
|
||||
}
|
||||
|
||||
switch (msg.Type)
|
||||
private void PrintWarnChat(string? message)
|
||||
{
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ")
|
||||
.AddUiForeground("Warning: " + (message ?? string.Empty), 31).AddUiForegroundOff();
|
||||
_chatGui.Print(se.BuiltString);
|
||||
}
|
||||
|
||||
private void HandlePairRequestsUpdated(PairRequestsUpdatedMessage _)
|
||||
{
|
||||
var activeRequests = _pairRequestService.GetActiveRequests();
|
||||
var activeRequestIds = activeRequests.Select(r => r.HashedCid).ToHashSet();
|
||||
|
||||
// Dismiss notifications for requests that are no longer active
|
||||
var notificationsToRemove = _shownPairRequestNotifications
|
||||
.Where(hashedCid => !activeRequestIds.Contains(hashedCid))
|
||||
.ToList();
|
||||
|
||||
foreach (var hashedCid in notificationsToRemove)
|
||||
{
|
||||
case NotificationType.Info:
|
||||
ShowNotificationLocationBased(msg, _configurationService.Current.InfoNotification);
|
||||
break;
|
||||
var notificationId = $"pair_request_{hashedCid}";
|
||||
Mediator.Publish(new LightlessNotificationDismissMessage(notificationId));
|
||||
_shownPairRequestNotifications.Remove(hashedCid);
|
||||
}
|
||||
|
||||
case NotificationType.Warning:
|
||||
ShowNotificationLocationBased(msg, _configurationService.Current.WarningNotification);
|
||||
break;
|
||||
|
||||
case NotificationType.Error:
|
||||
ShowNotificationLocationBased(msg, _configurationService.Current.ErrorNotification);
|
||||
break;
|
||||
// Show/update notifications for all active requests
|
||||
foreach (var request in activeRequests)
|
||||
{
|
||||
_shownPairRequestNotifications.Add(request.HashedCid);
|
||||
ShowPairRequestNotification(
|
||||
request.DisplayName,
|
||||
request.HashedCid,
|
||||
() => _pairRequestService.AcceptPairRequest(request.HashedCid, request.DisplayName),
|
||||
() => _pairRequestService.DeclinePairRequest(request.HashedCid)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location)
|
||||
{
|
||||
switch (location)
|
||||
{
|
||||
case NotificationLocation.Toast:
|
||||
ShowToast(msg);
|
||||
break;
|
||||
|
||||
case NotificationLocation.Chat:
|
||||
ShowChat(msg);
|
||||
break;
|
||||
|
||||
case NotificationLocation.Both:
|
||||
ShowToast(msg);
|
||||
ShowChat(msg);
|
||||
break;
|
||||
|
||||
case NotificationLocation.Nowhere:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowToast(NotificationMessage msg)
|
||||
{
|
||||
Dalamud.Interface.ImGuiNotification.NotificationType dalamudType = msg.Type switch
|
||||
{
|
||||
NotificationType.Error => Dalamud.Interface.ImGuiNotification.NotificationType.Error,
|
||||
NotificationType.Warning => Dalamud.Interface.ImGuiNotification.NotificationType.Warning,
|
||||
NotificationType.Info => Dalamud.Interface.ImGuiNotification.NotificationType.Info,
|
||||
_ => Dalamud.Interface.ImGuiNotification.NotificationType.Info
|
||||
};
|
||||
|
||||
_notificationManager.AddNotification(new Notification()
|
||||
{
|
||||
Content = msg.Message ?? string.Empty,
|
||||
Title = msg.Title,
|
||||
Type = dalamudType,
|
||||
Minimized = false,
|
||||
InitialDuration = msg.TimeShownOnScreen ?? TimeSpan.FromSeconds(3)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
220
LightlessSync/Services/PairProcessingLimiter.cs
Normal file
220
LightlessSync/Services/PairProcessingLimiter.cs
Normal file
@@ -0,0 +1,220 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
|
||||
public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private const int HardLimit = 32;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly object _limitLock = new();
|
||||
private readonly SemaphoreSlim _semaphore;
|
||||
private int _currentLimit;
|
||||
private int _pendingReductions;
|
||||
private int _waiting;
|
||||
private int _inFlight;
|
||||
|
||||
public PairProcessingLimiter(ILogger<PairProcessingLimiter> logger, LightlessMediator mediator, LightlessConfigService configService)
|
||||
: base(logger, mediator)
|
||||
{
|
||||
_configService = configService;
|
||||
_currentLimit = CalculateLimit();
|
||||
var initialCount = _configService.Current.EnablePairProcessingLimiter ? _currentLimit : HardLimit;
|
||||
_semaphore = new SemaphoreSlim(initialCount, HardLimit);
|
||||
|
||||
Mediator.Subscribe<PairProcessingLimitChangedMessage>(this, _ => UpdateSemaphoreLimit());
|
||||
}
|
||||
|
||||
public ValueTask<IAsyncDisposable> AcquireAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return WaitInternalAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public PairProcessingLimiterSnapshot GetSnapshot()
|
||||
{
|
||||
lock (_limitLock)
|
||||
{
|
||||
var enabled = IsEnabled;
|
||||
var limit = enabled ? _currentLimit : CalculateLimit();
|
||||
var waiting = Math.Max(0, Volatile.Read(ref _waiting));
|
||||
var inFlight = Math.Max(0, Volatile.Read(ref _inFlight));
|
||||
return new PairProcessingLimiterSnapshot(enabled, limit, inFlight, waiting);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsEnabled => _configService.Current.EnablePairProcessingLimiter;
|
||||
|
||||
private async ValueTask<IAsyncDisposable> WaitInternalAsync(CancellationToken token)
|
||||
{
|
||||
if (!IsEnabled)
|
||||
{
|
||||
return NoopReleaser.Instance;
|
||||
}
|
||||
|
||||
Interlocked.Increment(ref _waiting);
|
||||
try
|
||||
{
|
||||
await _semaphore.WaitAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Interlocked.Decrement(ref _waiting);
|
||||
throw;
|
||||
}
|
||||
|
||||
Interlocked.Decrement(ref _waiting);
|
||||
|
||||
if (!IsEnabled)
|
||||
{
|
||||
_semaphore.Release();
|
||||
return NoopReleaser.Instance;
|
||||
}
|
||||
|
||||
Interlocked.Increment(ref _inFlight);
|
||||
return new Releaser(this);
|
||||
}
|
||||
|
||||
private void UpdateSemaphoreLimit()
|
||||
{
|
||||
lock (_limitLock)
|
||||
{
|
||||
var enabled = IsEnabled;
|
||||
var desiredLimit = CalculateLimit();
|
||||
|
||||
if (!enabled)
|
||||
{
|
||||
var releaseAmount = HardLimit - _semaphore.CurrentCount;
|
||||
if (releaseAmount > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
_semaphore.Release(releaseAmount);
|
||||
}
|
||||
catch (SemaphoreFullException)
|
||||
{
|
||||
// ignore, already at max
|
||||
}
|
||||
}
|
||||
|
||||
_currentLimit = desiredLimit;
|
||||
_pendingReductions = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (desiredLimit == _currentLimit)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (desiredLimit > _currentLimit)
|
||||
{
|
||||
var increment = desiredLimit - _currentLimit;
|
||||
var allowed = Math.Min(increment, HardLimit - _semaphore.CurrentCount);
|
||||
if (allowed > 0)
|
||||
{
|
||||
_semaphore.Release(allowed);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var decrement = _currentLimit - desiredLimit;
|
||||
var removed = 0;
|
||||
while (removed < decrement && _semaphore.Wait(0))
|
||||
{
|
||||
removed++;
|
||||
}
|
||||
|
||||
var remaining = decrement - removed;
|
||||
if (remaining > 0)
|
||||
{
|
||||
_pendingReductions += remaining;
|
||||
}
|
||||
}
|
||||
|
||||
_currentLimit = desiredLimit;
|
||||
Logger.LogDebug("Pair processing concurrency updated to {limit} (pending reductions: {pending})", _currentLimit, _pendingReductions);
|
||||
}
|
||||
}
|
||||
|
||||
private int CalculateLimit()
|
||||
{
|
||||
var configured = _configService.Current.MaxConcurrentPairApplications;
|
||||
return Math.Clamp(configured, 1, HardLimit);
|
||||
}
|
||||
|
||||
private void ReleaseOne()
|
||||
{
|
||||
var inFlight = Interlocked.Decrement(ref _inFlight);
|
||||
if (inFlight < 0)
|
||||
{
|
||||
Interlocked.Exchange(ref _inFlight, 0);
|
||||
}
|
||||
|
||||
if (!IsEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_limitLock)
|
||||
{
|
||||
if (_pendingReductions > 0)
|
||||
{
|
||||
_pendingReductions--;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_semaphore.Release();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_semaphore.Dispose();
|
||||
}
|
||||
|
||||
private sealed class Releaser : IAsyncDisposable
|
||||
{
|
||||
private PairProcessingLimiter? _owner;
|
||||
|
||||
public Releaser(PairProcessingLimiter owner)
|
||||
{
|
||||
_owner = owner;
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
var owner = Interlocked.Exchange(ref _owner, null);
|
||||
owner?.ReleaseOne();
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class NoopReleaser : IAsyncDisposable
|
||||
{
|
||||
public static readonly NoopReleaser Instance = new();
|
||||
|
||||
private NoopReleaser()
|
||||
{
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct PairProcessingLimiterSnapshot(bool IsEnabled, int Limit, int InFlight, int Waiting)
|
||||
{
|
||||
public int Remaining => Math.Max(0, Limit - InFlight);
|
||||
}
|
||||
227
LightlessSync/Services/PairRequestService.cs
Normal file
227
LightlessSync/Services/PairRequestService.cs
Normal file
@@ -0,0 +1,227 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
|
||||
public sealed class PairRequestService : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly Lazy<WebAPI.ApiController> _apiController;
|
||||
private readonly object _syncRoot = new();
|
||||
private readonly List<PairRequestEntry> _requests = [];
|
||||
|
||||
private static readonly TimeSpan Expiration = TimeSpan.FromMinutes(5);
|
||||
|
||||
public PairRequestService(ILogger<PairRequestService> logger, LightlessMediator mediator, DalamudUtilService dalamudUtil, PairManager pairManager, Lazy<WebAPI.ApiController> apiController)
|
||||
: base(logger, mediator)
|
||||
{
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_pairManager = pairManager;
|
||||
_apiController = apiController;
|
||||
|
||||
Mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, _ =>
|
||||
{
|
||||
bool removed;
|
||||
lock (_syncRoot)
|
||||
{
|
||||
removed = CleanupExpiredUnsafe();
|
||||
}
|
||||
|
||||
if (removed)
|
||||
{
|
||||
Mediator.Publish(new PairRequestsUpdatedMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public PairRequestDisplay RegisterIncomingRequest(string hashedCid, string messageTemplate)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(hashedCid))
|
||||
{
|
||||
hashedCid = string.Empty;
|
||||
}
|
||||
|
||||
messageTemplate ??= string.Empty;
|
||||
|
||||
PairRequestEntry entry = new(hashedCid, messageTemplate, DateTime.UtcNow);
|
||||
lock (_syncRoot)
|
||||
{
|
||||
CleanupExpiredUnsafe();
|
||||
var index = _requests.FindIndex(r => string.Equals(r.HashedCid, hashedCid, StringComparison.Ordinal));
|
||||
if (index >= 0)
|
||||
{
|
||||
_requests[index] = entry;
|
||||
}
|
||||
else
|
||||
{
|
||||
_requests.Add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
var display = _dalamudUtil.IsOnFrameworkThread
|
||||
? ToDisplay(entry)
|
||||
: _dalamudUtil.RunOnFrameworkThread(() => ToDisplay(entry)).GetAwaiter().GetResult();
|
||||
|
||||
Mediator.Publish(new PairRequestsUpdatedMessage());
|
||||
return display;
|
||||
}
|
||||
|
||||
public IReadOnlyList<PairRequestDisplay> GetActiveRequests()
|
||||
{
|
||||
List<PairRequestEntry> entries;
|
||||
lock (_syncRoot)
|
||||
{
|
||||
CleanupExpiredUnsafe();
|
||||
entries = _requests
|
||||
.OrderByDescending(r => r.ReceivedAt)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
return _dalamudUtil.IsOnFrameworkThread
|
||||
? entries.Select(ToDisplay).ToList()
|
||||
: _dalamudUtil.RunOnFrameworkThread(() => entries.Select(ToDisplay).ToList()).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public bool RemoveRequest(string hashedCid)
|
||||
{
|
||||
bool removed;
|
||||
lock (_syncRoot)
|
||||
{
|
||||
removed = _requests.RemoveAll(r => string.Equals(r.HashedCid, hashedCid, StringComparison.Ordinal)) > 0;
|
||||
}
|
||||
|
||||
if (removed)
|
||||
{
|
||||
Mediator.Publish(new PairRequestsUpdatedMessage());
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
public bool HasPendingRequests()
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
CleanupExpiredUnsafe();
|
||||
return _requests.Count > 0;
|
||||
}
|
||||
}
|
||||
|
||||
private PairRequestDisplay ToDisplay(PairRequestEntry entry)
|
||||
{
|
||||
var displayName = ResolveDisplayName(entry.HashedCid);
|
||||
var message = FormatMessage(entry.MessageTemplate, displayName);
|
||||
return new PairRequestDisplay(entry.HashedCid, displayName, message, entry.ReceivedAt);
|
||||
}
|
||||
|
||||
private string ResolveDisplayName(string hashedCid)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(hashedCid))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var (name, address) = _dalamudUtil.FindPlayerByNameHash(hashedCid);
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
var worldName = _dalamudUtil.GetWorldNameFromPlayerAddress(address);
|
||||
return !string.IsNullOrWhiteSpace(worldName)
|
||||
? $"{name} @ {worldName}"
|
||||
: name;
|
||||
}
|
||||
|
||||
var pair = _pairManager
|
||||
.GetOnlineUserPairs()
|
||||
.FirstOrDefault(p => string.Equals(p.Ident, hashedCid, StringComparison.Ordinal));
|
||||
|
||||
if (pair != null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(pair.PlayerName))
|
||||
{
|
||||
return pair.PlayerName;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(pair.UserData.AliasOrUID))
|
||||
{
|
||||
return pair.UserData.AliasOrUID;
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static string FormatMessage(string template, string displayName)
|
||||
{
|
||||
var safeName = string.IsNullOrWhiteSpace(displayName) ? "Someone" : displayName;
|
||||
template ??= string.Empty;
|
||||
const string placeholder = "{DisplayName}";
|
||||
|
||||
if (!string.IsNullOrEmpty(template) && template.Contains(placeholder, StringComparison.Ordinal))
|
||||
{
|
||||
return template.Replace(placeholder, safeName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(template))
|
||||
{
|
||||
return $"{safeName}: {template}";
|
||||
}
|
||||
|
||||
return $"{safeName} sent you a pair request.";
|
||||
}
|
||||
|
||||
private bool CleanupExpiredUnsafe()
|
||||
{
|
||||
if (_requests.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
return _requests.RemoveAll(r => now - r.ReceivedAt > Expiration) > 0;
|
||||
}
|
||||
|
||||
public void AcceptPairRequest(string hashedCid, string displayName)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await _apiController.Value.TryPairWithContentId(hashedCid).ConfigureAwait(false);
|
||||
RemoveRequest(hashedCid);
|
||||
|
||||
var displayText = string.IsNullOrEmpty(displayName) ? hashedCid : displayName;
|
||||
Mediator.Publish(new NotificationMessage(
|
||||
"Pair request accepted",
|
||||
$"Sent a pair request back to {displayText}.",
|
||||
NotificationType.Info,
|
||||
TimeSpan.FromSeconds(3)));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to accept pair request for {HashedCid}", hashedCid);
|
||||
Mediator.Publish(new NotificationMessage(
|
||||
"Failed to Accept Pair Request",
|
||||
ex.Message,
|
||||
NotificationType.Error,
|
||||
TimeSpan.FromSeconds(5)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void DeclinePairRequest(string hashedCid)
|
||||
{
|
||||
RemoveRequest(hashedCid);
|
||||
Logger.LogDebug("Declined pair request from {HashedCid}", hashedCid);
|
||||
}
|
||||
|
||||
private record struct PairRequestEntry(string HashedCid, string MessageTemplate, DateTime ReceivedAt);
|
||||
|
||||
public readonly record struct PairRequestDisplay(string HashedCid, string DisplayName, string Message, DateTime ReceivedAt);
|
||||
}
|
||||
@@ -504,7 +504,7 @@ public class ServerConfigurationManager
|
||||
|
||||
internal void RenameTag(Dictionary<string, List<string>> tags, HashSet<string> storage, string oldName, string newName)
|
||||
{
|
||||
if (newName.Length > _maxCharactersFolder)
|
||||
if (newName.Length < _maxCharactersFolder)
|
||||
{
|
||||
storage.Remove(oldName);
|
||||
storage.Add(newName);
|
||||
@@ -607,8 +607,9 @@ public class ServerConfigurationManager
|
||||
{
|
||||
var baseUri = serverUri.Replace("wss://", "https://").Replace("ws://", "http://");
|
||||
var oauthCheckUri = LightlessAuth.GetUIDsFullPath(new Uri(baseUri));
|
||||
_httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||
var response = await _httpClient.GetAsync(oauthCheckUri).ConfigureAwait(false);
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, oauthCheckUri);
|
||||
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||
using var response = await _httpClient.SendAsync(request).ConfigureAwait(false);
|
||||
var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||
return await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(responseStream).ConfigureAwait(false) ?? [];
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.ImGuiFileDialog;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI;
|
||||
using LightlessSync.UI.Style;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
@@ -22,7 +23,8 @@ public sealed class UiService : DisposableMediatorSubscriberBase
|
||||
LightlessConfigService lightlessConfigService, WindowSystem windowSystem,
|
||||
IEnumerable<WindowMediatorSubscriberBase> windows,
|
||||
UiFactory uiFactory, FileDialogManager fileDialogManager,
|
||||
LightlessMediator lightlessMediator) : base(logger, lightlessMediator)
|
||||
LightlessMediator lightlessMediator,
|
||||
NotificationService notificationService) : base(logger, lightlessMediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_logger.LogTrace("Creating {type}", GetType().Name);
|
||||
@@ -119,7 +121,15 @@ public sealed class UiService : DisposableMediatorSubscriberBase
|
||||
|
||||
private void Draw()
|
||||
{
|
||||
_windowSystem.Draw();
|
||||
_fileDialogManager.Draw();
|
||||
MainStyle.PushStyle();
|
||||
try
|
||||
{
|
||||
_windowSystem.Draw();
|
||||
_fileDialogManager.Draw();
|
||||
}
|
||||
finally
|
||||
{
|
||||
MainStyle.PopStyle();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Utility;
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.WebAPI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Numerics;
|
||||
@@ -22,7 +25,7 @@ namespace LightlessSync.UI
|
||||
private IReadOnlyList<GroupFullInfoDto> _allSyncshells;
|
||||
private string _userUid = string.Empty;
|
||||
|
||||
private List<(string Label, string? GID, bool IsAvailable)> _syncshellOptions = new();
|
||||
private readonly List<(string Label, string? GID, bool IsAvailable)> _syncshellOptions = new();
|
||||
|
||||
public BroadcastUI(
|
||||
ILogger<BroadcastUI> logger,
|
||||
@@ -44,11 +47,9 @@ namespace LightlessSync.UI
|
||||
IsOpen = false;
|
||||
this.SizeConstraints = new()
|
||||
{
|
||||
MinimumSize = new(600, 340),
|
||||
MaximumSize = new(750, 400)
|
||||
MinimumSize = new(600, 465),
|
||||
MaximumSize = new(750, 525)
|
||||
};
|
||||
|
||||
mediator.Subscribe<RefreshUiMessage>(this, async _ => await RefreshSyncshells());
|
||||
}
|
||||
|
||||
private void RebuildSyncshellDropdownOptions()
|
||||
@@ -62,7 +63,7 @@ namespace LightlessSync.UI
|
||||
_syncshellOptions.Clear();
|
||||
_syncshellOptions.Add(("None", null, true));
|
||||
|
||||
var addedGids = new HashSet<string>();
|
||||
var addedGids = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var shell in ownedSyncshells)
|
||||
{
|
||||
@@ -73,7 +74,7 @@ namespace LightlessSync.UI
|
||||
|
||||
if (!string.IsNullOrEmpty(selectedGid) && !addedGids.Contains(selectedGid))
|
||||
{
|
||||
var matching = allSyncshells.FirstOrDefault(g => g.GID == selectedGid);
|
||||
var matching = allSyncshells.FirstOrDefault(g => string.Equals(g.GID, selectedGid, StringComparison.Ordinal));
|
||||
if (matching != null)
|
||||
{
|
||||
var label = matching.GroupAliasOrGID ?? matching.GID;
|
||||
@@ -97,7 +98,7 @@ namespace LightlessSync.UI
|
||||
{
|
||||
if (!_apiController.IsConnected)
|
||||
{
|
||||
_allSyncshells = Array.Empty<GroupFullInfoDto>();
|
||||
_allSyncshells = [];
|
||||
RebuildSyncshellDropdownOptions();
|
||||
return;
|
||||
}
|
||||
@@ -109,7 +110,7 @@ namespace LightlessSync.UI
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to fetch Syncshells.");
|
||||
_allSyncshells = Array.Empty<GroupFullInfoDto>();
|
||||
_allSyncshells = [];
|
||||
}
|
||||
|
||||
RebuildSyncshellDropdownOptions();
|
||||
@@ -119,7 +120,7 @@ namespace LightlessSync.UI
|
||||
public override void OnOpen()
|
||||
{
|
||||
_userUid = _apiController.UID;
|
||||
_ = RefreshSyncshellsInternal();
|
||||
_ = RefreshSyncshells();
|
||||
}
|
||||
|
||||
protected override void DrawInternal()
|
||||
@@ -131,25 +132,66 @@ namespace LightlessSync.UI
|
||||
ImGuiHelpers.ScaledDummy(0.25f);
|
||||
}
|
||||
|
||||
if (ImGui.BeginTabBar("##MyTabBar"))
|
||||
if (ImGui.BeginTabBar("##BroadcastTabs"))
|
||||
{
|
||||
if (ImGui.BeginTabItem("Lightfinder"))
|
||||
{
|
||||
_uiSharedService.MediumText("Lightfinder", UIColors.Get("PairBlue"));
|
||||
|
||||
ImGui.PushTextWrapPos();
|
||||
ImGui.Text("This lets other Lightless users know you use Lightless.");
|
||||
ImGui.Text("By enabling this, the server will allow other people to see that you are using Lightless.");
|
||||
ImGui.Text("When disabled, pairing is still possible but both parties need to mutually send each other requests, receiving party will not be notified about the request unless the pairing is complete.");
|
||||
ImGui.Text("At no point ever, even when Lightfinder is active that any Lightless data is getting sent to other people (including ID's), the server keeps this to itself.");
|
||||
ImGui.Text("You can request to pair by right-clicking any (not yourself) character and using 'Send Pair Request'.");
|
||||
ImGui.PopTextWrapPos();
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1, -2));
|
||||
|
||||
_uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"),"This lets other Lightless users know you use Lightless. While enabled, you and others using Lightfinder can see each other identified as Lightless users.");
|
||||
|
||||
ImGui.Indent(15f);
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
|
||||
ImGui.Text("- This is done using a 'Lightless' label above player nameplates.");
|
||||
ImGui.PopStyleColor();
|
||||
ImGui.Unindent(15f);
|
||||
|
||||
ImGuiHelpers.ScaledDummy(3f);
|
||||
|
||||
_uiSharedService.MediumText("Pairing", UIColors.Get("PairBlue"));
|
||||
_uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "Pairing may be initiated via the right-click context menu on another player." +
|
||||
" The process requires mutual confirmation: the sender initiates the request, and the recipient completes it by responding with a request in return.");
|
||||
|
||||
_uiSharedService.DrawNoteLine(
|
||||
"! ",
|
||||
UIColors.Get("LightlessYellow"),
|
||||
new SeStringUtils.RichTextEntry("If Lightfinder is "),
|
||||
new SeStringUtils.RichTextEntry("ENABLED", UIColors.Get("LightlessGreen"), true),
|
||||
new SeStringUtils.RichTextEntry(" when a pair request is made, the receiving user will get notified about it."));
|
||||
|
||||
_uiSharedService.DrawNoteLine(
|
||||
"! ",
|
||||
UIColors.Get("LightlessYellow"),
|
||||
new SeStringUtils.RichTextEntry("If Lightfinder is "),
|
||||
new SeStringUtils.RichTextEntry("DISABLED", UIColors.Get("DimRed"), true),
|
||||
new SeStringUtils.RichTextEntry(" when a pair request is made, the receiving user will "),
|
||||
new SeStringUtils.RichTextEntry("NOT", UIColors.Get("DimRed"), true),
|
||||
new SeStringUtils.RichTextEntry(" get a notification, and the request will not be visible to them in any way."));
|
||||
|
||||
ImGuiHelpers.ScaledDummy(3f);
|
||||
|
||||
_uiSharedService.MediumText("Privacy", UIColors.Get("PairBlue"));
|
||||
|
||||
_uiSharedService.DrawNoteLine(
|
||||
"! ",
|
||||
UIColors.Get("DimRed"),
|
||||
new SeStringUtils.RichTextEntry("Lightfinder is entirely "),
|
||||
new SeStringUtils.RichTextEntry("opt-in", UIColors.Get("LightlessYellow"), true),
|
||||
new SeStringUtils.RichTextEntry(" and does not share any data with other users. All identifying information remains private to the server."));
|
||||
|
||||
_uiSharedService.DrawNoteLine("! ", UIColors.Get("DimRed"), "Pairing is intended as a mutual agreement between both parties. A pair request will not be visible to the recipient unless Lightfinder is enabled.");
|
||||
|
||||
ImGuiHelpers.ScaledDummy(5f);
|
||||
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
|
||||
ImGui.Text("Use it only when you want to be visible.");
|
||||
ImGui.TextWrapped("Use Lightfinder when you're okay with being visible to other users and understand that you are responsible for your own experience.");
|
||||
ImGui.PopStyleColor();
|
||||
|
||||
ImGuiHelpers.ScaledDummy(0.2f);
|
||||
ImGui.PopStyleVar();
|
||||
|
||||
ImGuiHelpers.ScaledDummy(3f);
|
||||
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
|
||||
|
||||
if (_configService.Current.BroadcastEnabled)
|
||||
@@ -211,12 +253,27 @@ namespace LightlessSync.UI
|
||||
_broadcastService.ToggleBroadcast();
|
||||
}
|
||||
|
||||
var toggleButtonHeight = ImGui.GetItemRectSize().Y;
|
||||
|
||||
if (isOnCooldown || !_broadcastService.IsLightFinderAvailable)
|
||||
ImGui.EndDisabled();
|
||||
|
||||
ImGui.PopStyleColor();
|
||||
ImGui.PopStyleVar();
|
||||
|
||||
ImGui.SameLine();
|
||||
if (_uiSharedService.IconButton(FontAwesomeIcon.Cog, toggleButtonHeight))
|
||||
{
|
||||
Mediator.Publish(new OpenLightfinderSettingsMessage());
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
ImGui.TextUnformatted("Open Lightfinder settings.");
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
|
||||
@@ -225,6 +282,7 @@ namespace LightlessSync.UI
|
||||
if (_allSyncshells == null)
|
||||
{
|
||||
ImGui.Text("Loading Syncshells...");
|
||||
_ = RefreshSyncshells();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -260,14 +318,14 @@ namespace LightlessSync.UI
|
||||
}
|
||||
|
||||
var selectedGid = _configService.Current.SelectedFinderSyncshell;
|
||||
var currentOption = _syncshellOptions.FirstOrDefault(o => o.GID == selectedGid);
|
||||
var currentOption = _syncshellOptions.FirstOrDefault(o => string.Equals(o.GID, selectedGid, StringComparison.Ordinal));
|
||||
var preview = currentOption.Label ?? "Select a Syncshell...";
|
||||
|
||||
if (ImGui.BeginCombo("##SyncshellDropdown", preview))
|
||||
{
|
||||
foreach (var (label, gid, available) in _syncshellOptions)
|
||||
{
|
||||
bool isSelected = gid == selectedGid;
|
||||
bool isSelected = string.Equals(gid, selectedGid, StringComparison.Ordinal);
|
||||
|
||||
if (!available)
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
|
||||
@@ -276,6 +334,7 @@ namespace LightlessSync.UI
|
||||
{
|
||||
_configService.Current.SelectedFinderSyncshell = gid;
|
||||
_configService.Save();
|
||||
_ = RefreshSyncshells();
|
||||
}
|
||||
|
||||
if (!available && ImGui.IsItemHovered())
|
||||
@@ -310,6 +369,7 @@ namespace LightlessSync.UI
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
if (ImGui.BeginTabItem("Debug"))
|
||||
{
|
||||
ImGui.Text("Broadcast Cache");
|
||||
@@ -366,17 +426,12 @@ namespace LightlessSync.UI
|
||||
ImGui.EndTable();
|
||||
}
|
||||
|
||||
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
#endif
|
||||
|
||||
ImGui.EndTabBar();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using System;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
@@ -24,6 +25,7 @@ using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
|
||||
@@ -85,7 +87,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
IpcManager ipcManager,
|
||||
BroadcastService broadcastService,
|
||||
CharacterAnalyzer characterAnalyzer,
|
||||
PlayerPerformanceConfigService playerPerformanceConfig) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService)
|
||||
PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService)
|
||||
{
|
||||
_uiSharedService = uiShared;
|
||||
_configService = configService;
|
||||
@@ -103,7 +105,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
_renamePairTagUi = renameTagUi;
|
||||
_ipcManager = ipcManager;
|
||||
_broadcastService = broadcastService;
|
||||
_tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService);
|
||||
_tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService, dalamudUtilService, lightlessNotificationService);
|
||||
|
||||
AllowPinning = true;
|
||||
AllowClickthrough = false;
|
||||
@@ -141,7 +143,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
},
|
||||
};
|
||||
|
||||
_drawFolders = [.. GetDrawFolders()];
|
||||
_drawFolders = [.. DrawFolders];
|
||||
|
||||
#if DEBUG
|
||||
string dev = "Dev Build";
|
||||
@@ -158,7 +160,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => UiSharedService_GposeEnd());
|
||||
Mediator.Subscribe<DownloadStartedMessage>(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus);
|
||||
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _));
|
||||
Mediator.Subscribe<RefreshUiMessage>(this, (msg) => _drawFolders = GetDrawFolders().ToList());
|
||||
Mediator.Subscribe<RefreshUiMessage>(this, (msg) => _drawFolders = DrawFolders.ToList());
|
||||
|
||||
Flags |= ImGuiWindowFlags.NoDocking;
|
||||
|
||||
@@ -375,7 +377,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
|
||||
private void DrawTransfers()
|
||||
{
|
||||
var currentUploads = _fileTransferManager.CurrentUploads.ToList();
|
||||
var currentUploads = _fileTransferManager.GetCurrentUploadsSnapshot();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
_uiSharedService.IconText(FontAwesomeIcon.Upload);
|
||||
ImGui.SameLine(35 * ImGuiHelpers.GlobalScale);
|
||||
@@ -385,10 +387,12 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
var totalUploads = currentUploads.Count;
|
||||
|
||||
var doneUploads = currentUploads.Count(c => c.IsTransferred);
|
||||
var activeUploads = currentUploads.Count(c => !c.IsTransferred);
|
||||
var uploadSlotLimit = Math.Clamp(_configService.Current.ParallelUploads, 1, 8);
|
||||
var totalUploaded = currentUploads.Sum(c => c.Transferred);
|
||||
var totalToUpload = currentUploads.Sum(c => c.Total);
|
||||
|
||||
ImGui.TextUnformatted($"{doneUploads}/{totalUploads}");
|
||||
ImGui.TextUnformatted($"{doneUploads}/{totalUploads} (slots {activeUploads}/{uploadSlotLimit})");
|
||||
var uploadText = $"({UiSharedService.ByteToString(totalUploaded)}/{UiSharedService.ByteToString(totalToUpload)})";
|
||||
var textSize = ImGui.CalcTextSize(uploadText);
|
||||
ImGui.SameLine(_windowContentWidth - textSize.X);
|
||||
@@ -401,7 +405,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
ImGui.TextUnformatted("No uploads in progress");
|
||||
}
|
||||
|
||||
var currentDownloads = _currentDownloads.SelectMany(d => d.Value.Values).ToList();
|
||||
var currentDownloads = BuildCurrentDownloadSnapshot();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
_uiSharedService.IconText(FontAwesomeIcon.Download);
|
||||
ImGui.SameLine(35 * ImGuiHelpers.GlobalScale);
|
||||
@@ -428,10 +432,53 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private List<FileDownloadStatus> BuildCurrentDownloadSnapshot()
|
||||
{
|
||||
List<FileDownloadStatus> snapshot = new();
|
||||
|
||||
foreach (var kvp in _currentDownloads.ToArray())
|
||||
{
|
||||
var value = kvp.Value;
|
||||
if (value == null || value.Count == 0)
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
snapshot.AddRange(value.Values.ToArray());
|
||||
}
|
||||
catch (System.ArgumentException)
|
||||
{
|
||||
// skibidi
|
||||
}
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private void DrawUIDHeader()
|
||||
{
|
||||
var uidText = GetUidText();
|
||||
|
||||
Vector4? vanityTextColor = null;
|
||||
Vector4? vanityGlowColor = null;
|
||||
bool useVanityColors = false;
|
||||
|
||||
if (_configService.Current.useColoredUIDs && _apiController.HasVanity)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_apiController.TextColorHex))
|
||||
{
|
||||
vanityTextColor = UIColors.HexToRgba(_apiController.TextColorHex);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_apiController.TextGlowColorHex))
|
||||
{
|
||||
vanityGlowColor = UIColors.HexToRgba(_apiController.TextGlowColorHex);
|
||||
}
|
||||
|
||||
useVanityColors = vanityTextColor is not null || vanityGlowColor is not null;
|
||||
}
|
||||
|
||||
//Getting information of character and triangles threshold to show overlimit status in UID bar.
|
||||
_cachedAnalysis = _characterAnalyzer.LastAnalysis.DeepClone();
|
||||
|
||||
@@ -444,9 +491,9 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
|
||||
float contentWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
|
||||
float uidStartX = (contentWidth - uidTextSize.X) / 2f;
|
||||
float cursorY = ImGui.GetCursorPosY();
|
||||
float cursorY = ImGui.GetCursorPosY();
|
||||
|
||||
if (_configService.Current.BroadcastEnabled)
|
||||
if (_configService.Current.BroadcastEnabled && _apiController.IsConnected)
|
||||
{
|
||||
float iconYOffset = (uidTextSize.Y - iconSize.Y) * 0.5f;
|
||||
var buttonSize = new Vector2(iconSize.X, uidTextSize.Y);
|
||||
@@ -467,14 +514,8 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
ImGui.Text("Lightfinder");
|
||||
ImGui.PopStyleColor();
|
||||
|
||||
ImGui.Text("This lets other Lightless users know you use Lightless.");
|
||||
ImGui.Text("By enabling this, the server will allow other people to see that you are using Lightless.");
|
||||
ImGui.Text("When disabled, pairing is still possible but both parties need to mutually send each other requests, receiving party will not be notified about the request unless the pairing is complete.");
|
||||
ImGui.Text("At no point ever, even when Lightfinder is active that any Lightless data is getting sent to other people (including ID's), the server keeps this to itself.");
|
||||
ImGui.Text("You can request to pair by right-clicking any (not yourself) character and using 'Send Pair Request'.");
|
||||
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
|
||||
ImGui.Text("Use it only when you want to be visible.");
|
||||
ImGui.TextWrapped("Use Lightfinder when you're okay with being visible to other users and understand that you are responsible for your own experience.");
|
||||
ImGui.PopStyleColor();
|
||||
|
||||
ImGuiHelpers.ScaledDummy(0.2f);
|
||||
@@ -524,12 +565,30 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
|
||||
ImGui.SetCursorPosY(cursorY);
|
||||
ImGui.SetCursorPosX(uidStartX);
|
||||
|
||||
bool headerItemClicked;
|
||||
using (_uiSharedService.UidFont.Push())
|
||||
{
|
||||
ImGui.TextColored(GetUidColor(), uidText);
|
||||
if (ImGui.IsItemClicked())
|
||||
ImGui.SetClipboardText(uidText);
|
||||
if (useVanityColors)
|
||||
{
|
||||
var seString = SeStringUtils.BuildFormattedPlayerName(uidText, vanityTextColor, vanityGlowColor);
|
||||
var cursorPos = ImGui.GetCursorScreenPos();
|
||||
var fontPtr = ImGui.GetFont();
|
||||
SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, fontPtr);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextColored(GetUidColor(), uidText);
|
||||
}
|
||||
}
|
||||
|
||||
headerItemClicked = ImGui.IsItemClicked();
|
||||
|
||||
if (headerItemClicked)
|
||||
{
|
||||
ImGui.SetClipboardText(uidText);
|
||||
}
|
||||
|
||||
UiSharedService.AttachToolTip("Click to copy");
|
||||
|
||||
if (_cachedAnalysis != null && _apiController.ServerState is ServerState.Connected)
|
||||
@@ -561,8 +620,9 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
if ((isOverTriHold || isOverVRAMUsage) && _playerPerformanceConfig.Current.WarnOnExceedingThresholds)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImGui.SetCursorPosY(cursorY + 15f);
|
||||
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow"));
|
||||
|
||||
|
||||
string warningMessage = "";
|
||||
if (isOverTriHold)
|
||||
{
|
||||
@@ -588,7 +648,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
|
||||
if (_apiController.ServerState is ServerState.Connected)
|
||||
{
|
||||
if (ImGui.IsItemClicked())
|
||||
if (headerItemClicked)
|
||||
{
|
||||
ImGui.SetClipboardText(_apiController.DisplayName);
|
||||
}
|
||||
@@ -597,11 +657,24 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
var origTextSize = ImGui.CalcTextSize(_apiController.UID);
|
||||
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X) / 2 - (origTextSize.X / 2));
|
||||
ImGui.TextColored(GetUidColor(), _apiController.UID);
|
||||
UiSharedService.AttachToolTip("Click to copy");
|
||||
if (ImGui.IsItemClicked())
|
||||
|
||||
if (useVanityColors)
|
||||
{
|
||||
_lightlessMediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi)));
|
||||
var seString = SeStringUtils.BuildFormattedPlayerName(_apiController.UID, vanityTextColor, vanityGlowColor);
|
||||
var cursorPos = ImGui.GetCursorScreenPos();
|
||||
var fontPtr = ImGui.GetFont();
|
||||
SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, fontPtr);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextColored(GetUidColor(), _apiController.UID);
|
||||
}
|
||||
|
||||
bool uidFooterClicked = ImGui.IsItemClicked();
|
||||
UiSharedService.AttachToolTip("Click to copy");
|
||||
if (uidFooterClicked)
|
||||
{
|
||||
ImGui.SetClipboardText(_apiController.UID);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -611,168 +684,166 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<IDrawFolder> GetDrawFolders()
|
||||
private IEnumerable<IDrawFolder> DrawFolders
|
||||
{
|
||||
List<IDrawFolder> drawFolders = [];
|
||||
|
||||
var allPairs = _pairManager.PairsWithGroups
|
||||
.ToDictionary(k => k.Key, k => k.Value);
|
||||
var filteredPairs = allPairs
|
||||
.Where(p =>
|
||||
{
|
||||
if (_tabMenu.Filter.IsNullOrEmpty()) return true;
|
||||
return p.Key.UserData.AliasOrUID.Contains(_tabMenu.Filter, StringComparison.OrdinalIgnoreCase) ||
|
||||
(p.Key.GetNote()?.Contains(_tabMenu.Filter, StringComparison.OrdinalIgnoreCase) ?? false) ||
|
||||
(p.Key.PlayerName?.Contains(_tabMenu.Filter, StringComparison.OrdinalIgnoreCase) ?? false);
|
||||
})
|
||||
.ToDictionary(k => k.Key, k => k.Value);
|
||||
|
||||
string? AlphabeticalSort(KeyValuePair<Pair, List<GroupFullInfoDto>> u)
|
||||
=> (_configService.Current.ShowCharacterNameInsteadOfNotesForVisible && !string.IsNullOrEmpty(u.Key.PlayerName)
|
||||
? (_configService.Current.PreferNotesOverNamesForVisible ? u.Key.GetNote() : u.Key.PlayerName)
|
||||
: (u.Key.GetNote() ?? u.Key.UserData.AliasOrUID));
|
||||
bool FilterOnlineOrPausedSelf(KeyValuePair<Pair, List<GroupFullInfoDto>> u)
|
||||
=> (u.Key.IsOnline || (!u.Key.IsOnline && !_configService.Current.ShowOfflineUsersSeparately)
|
||||
|| u.Key.UserPair.OwnPermissions.IsPaused());
|
||||
Dictionary<Pair, List<GroupFullInfoDto>> BasicSortedDictionary(IEnumerable<KeyValuePair<Pair, List<GroupFullInfoDto>>> u)
|
||||
=> u.OrderByDescending(u => u.Key.IsVisible)
|
||||
.ThenByDescending(u => u.Key.IsOnline)
|
||||
.ThenBy(AlphabeticalSort, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(u => u.Key, u => u.Value);
|
||||
ImmutableList<Pair> ImmutablePairList(IEnumerable<KeyValuePair<Pair, List<GroupFullInfoDto>>> u)
|
||||
=> u.Select(k => k.Key).ToImmutableList();
|
||||
bool FilterVisibleUsers(KeyValuePair<Pair, List<GroupFullInfoDto>> u)
|
||||
=> u.Key.IsVisible
|
||||
&& (_configService.Current.ShowSyncshellUsersInVisible || !(!_configService.Current.ShowSyncshellUsersInVisible && !u.Key.IsDirectlyPaired));
|
||||
bool FilterTagUsers(KeyValuePair<Pair, List<GroupFullInfoDto>> u, string tag)
|
||||
=> u.Key.IsDirectlyPaired && !u.Key.IsOneSidedPair && _tagHandler.HasPairTag(u.Key.UserData.UID, tag);
|
||||
bool FilterGroupUsers(KeyValuePair<Pair, List<GroupFullInfoDto>> u, GroupFullInfoDto group)
|
||||
=> u.Value.Exists(g => string.Equals(g.GID, group.GID, StringComparison.Ordinal));
|
||||
bool FilterNotTaggedUsers(KeyValuePair<Pair, List<GroupFullInfoDto>> u)
|
||||
=> u.Key.IsDirectlyPaired && !u.Key.IsOneSidedPair && !_tagHandler.HasAnyPairTag(u.Key.UserData.UID);
|
||||
bool FilterNotTaggedSyncshells(GroupFullInfoDto group)
|
||||
=> (!_tagHandler.HasAnySyncshellTag(group.GID) && !_configService.Current.ShowGroupedSyncshellsInAll) || true;
|
||||
bool FilterOfflineUsers(KeyValuePair<Pair, List<GroupFullInfoDto>> u)
|
||||
=> ((u.Key.IsDirectlyPaired && _configService.Current.ShowSyncshellOfflineUsersSeparately)
|
||||
|| !_configService.Current.ShowSyncshellOfflineUsersSeparately)
|
||||
&& (!u.Key.IsOneSidedPair || u.Value.Any()) && !u.Key.IsOnline && !u.Key.UserPair.OwnPermissions.IsPaused();
|
||||
bool FilterOfflineSyncshellUsers(KeyValuePair<Pair, List<GroupFullInfoDto>> u)
|
||||
=> (!u.Key.IsDirectlyPaired && !u.Key.IsOnline && !u.Key.UserPair.OwnPermissions.IsPaused());
|
||||
|
||||
|
||||
if (_configService.Current.ShowVisibleUsersSeparately)
|
||||
get
|
||||
{
|
||||
var allVisiblePairs = ImmutablePairList(allPairs
|
||||
.Where(FilterVisibleUsers));
|
||||
var filteredVisiblePairs = BasicSortedDictionary(filteredPairs
|
||||
.Where(FilterVisibleUsers));
|
||||
var drawFolders = new List<IDrawFolder>();
|
||||
var filter = _tabMenu.Filter;
|
||||
|
||||
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomVisibleTag, filteredVisiblePairs, allVisiblePairs));
|
||||
}
|
||||
var allPairs = _pairManager.PairsWithGroups.ToDictionary(k => k.Key, k => k.Value);
|
||||
var filteredPairs = allPairs.Where(p => PassesFilter(p.Key, filter)).ToDictionary(k => k.Key, k => k.Value);
|
||||
|
||||
List<IDrawFolder> groupFolders = new();
|
||||
|
||||
foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
GetGroups(allPairs, filteredPairs, group, out ImmutableList<Pair> allGroupPairs, out Dictionary<Pair, List<GroupFullInfoDto>> filteredGroupPairs);
|
||||
if (FilterNotTaggedSyncshells(group))
|
||||
//Filter of online/visible pairs
|
||||
if (_configService.Current.ShowVisibleUsersSeparately)
|
||||
{
|
||||
groupFolders.Add(_drawEntityFactory.CreateDrawGroupFolder(group, filteredGroupPairs, allGroupPairs));
|
||||
var allVisiblePairs = ImmutablePairList(allPairs.Where(p => FilterVisibleUsers(p.Key)));
|
||||
var filteredVisiblePairs = BasicSortedDictionary(filteredPairs.Where(p => FilterVisibleUsers(p.Key)));
|
||||
|
||||
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomVisibleTag, filteredVisiblePairs, allVisiblePairs));
|
||||
}
|
||||
}
|
||||
|
||||
if (_configService.Current.GroupUpSyncshells)
|
||||
drawFolders.Add(new DrawGroupedGroupFolder(groupFolders, _tagHandler, _uiSharedService, _selectSyncshellForTagUi, _renameSyncshellTagUi, ""));
|
||||
else
|
||||
drawFolders.AddRange(groupFolders);
|
||||
|
||||
var tags = _tagHandler.GetAllPairTagsSorted();
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
var allTagPairs = ImmutablePairList(allPairs
|
||||
.Where(u => FilterTagUsers(u, tag)));
|
||||
var filteredTagPairs = BasicSortedDictionary(filteredPairs
|
||||
.Where(u => FilterTagUsers(u, tag) && FilterOnlineOrPausedSelf(u)));
|
||||
|
||||
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(tag, filteredTagPairs, allTagPairs));
|
||||
}
|
||||
|
||||
var syncshellTags = _tagHandler.GetAllSyncshellTagsSorted();
|
||||
foreach (var syncshelltag in syncshellTags)
|
||||
{
|
||||
List<IDrawFolder> syncshellFolderTags = [];
|
||||
//Filter of not foldered syncshells
|
||||
var groupFolders = new List<IDrawFolder>();
|
||||
foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (_tagHandler.HasSyncshellTag(group.GID, syncshelltag))
|
||||
GetGroups(allPairs, filteredPairs, group, out ImmutableList<Pair> allGroupPairs, out Dictionary<Pair, List<GroupFullInfoDto>> filteredGroupPairs);
|
||||
|
||||
if (FilterNotTaggedSyncshells(group))
|
||||
{
|
||||
GetGroups(allPairs, filteredPairs, group, out ImmutableList<Pair> allGroupPairs, out Dictionary<Pair, List<GroupFullInfoDto>> filteredGroupPairs);
|
||||
syncshellFolderTags.Add(_drawEntityFactory.CreateDrawGroupFolder($"tag_{group.GID}", group, filteredGroupPairs, allGroupPairs));
|
||||
groupFolders.Add(_drawEntityFactory.CreateDrawGroupFolder(group, filteredGroupPairs, allGroupPairs));
|
||||
}
|
||||
}
|
||||
|
||||
if (syncshellFolderTags.Count > 0)
|
||||
//Filter of grouped up syncshells (All Syncshells Folder)
|
||||
if (_configService.Current.GroupUpSyncshells)
|
||||
drawFolders.Add(new DrawGroupedGroupFolder(groupFolders, _tagHandler, _uiSharedService,
|
||||
_selectSyncshellForTagUi, _renameSyncshellTagUi, ""));
|
||||
else
|
||||
drawFolders.AddRange(groupFolders);
|
||||
|
||||
//Filter of grouped/foldered pairs
|
||||
foreach (var tag in _tagHandler.GetAllPairTagsSorted())
|
||||
{
|
||||
drawFolders.Add(new DrawGroupedGroupFolder(syncshellFolderTags, _tagHandler, _uiSharedService, _selectSyncshellForTagUi, _renameSyncshellTagUi, syncshelltag));
|
||||
var allTagPairs = ImmutablePairList(allPairs.Where(p => FilterTagUsers(p.Key, tag)));
|
||||
var filteredTagPairs = BasicSortedDictionary(filteredPairs.Where(p => FilterTagUsers(p.Key, tag) && FilterOnlineOrPausedSelf(p.Key)));
|
||||
|
||||
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(tag, filteredTagPairs, allTagPairs));
|
||||
}
|
||||
}
|
||||
|
||||
var allOnlineNotTaggedPairs = ImmutablePairList(allPairs
|
||||
.Where(FilterNotTaggedUsers));
|
||||
var onlineNotTaggedPairs = BasicSortedDictionary(filteredPairs
|
||||
.Where(u => FilterNotTaggedUsers(u) && FilterOnlineOrPausedSelf(u)));
|
||||
|
||||
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder((_configService.Current.ShowOfflineUsersSeparately ? TagHandler.CustomOnlineTag : TagHandler.CustomAllTag),
|
||||
onlineNotTaggedPairs, allOnlineNotTaggedPairs));
|
||||
|
||||
if (_configService.Current.ShowOfflineUsersSeparately)
|
||||
{
|
||||
var allOfflinePairs = ImmutablePairList(allPairs
|
||||
.Where(FilterOfflineUsers));
|
||||
var filteredOfflinePairs = BasicSortedDictionary(filteredPairs
|
||||
.Where(FilterOfflineUsers));
|
||||
|
||||
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineTag, filteredOfflinePairs, allOfflinePairs));
|
||||
if (_configService.Current.ShowSyncshellOfflineUsersSeparately)
|
||||
//Filter of grouped/foldered syncshells
|
||||
foreach (var syncshellTag in _tagHandler.GetAllSyncshellTagsSorted())
|
||||
{
|
||||
var allOfflineSyncshellUsers = ImmutablePairList(allPairs
|
||||
.Where(FilterOfflineSyncshellUsers));
|
||||
var filteredOfflineSyncshellUsers = BasicSortedDictionary(filteredPairs
|
||||
.Where(FilterOfflineSyncshellUsers));
|
||||
|
||||
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineSyncshellTag,
|
||||
filteredOfflineSyncshellUsers,
|
||||
allOfflineSyncshellUsers));
|
||||
}
|
||||
}
|
||||
|
||||
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomUnpairedTag,
|
||||
BasicSortedDictionary(filteredPairs.Where(u => u.Key.IsOneSidedPair)),
|
||||
ImmutablePairList(allPairs.Where(u => u.Key.IsOneSidedPair))));
|
||||
|
||||
return drawFolders;
|
||||
|
||||
void GetGroups(Dictionary<Pair, List<GroupFullInfoDto>> allPairs, Dictionary<Pair, List<GroupFullInfoDto>> filteredPairs, GroupFullInfoDto group, out ImmutableList<Pair> allGroupPairs, out Dictionary<Pair, List<GroupFullInfoDto>> filteredGroupPairs)
|
||||
{
|
||||
allGroupPairs = ImmutablePairList(allPairs
|
||||
.Where(u => FilterGroupUsers(u, group)));
|
||||
filteredGroupPairs = filteredPairs
|
||||
.Where(u => FilterGroupUsers(u, group) && FilterOnlineOrPausedSelf(u))
|
||||
.OrderByDescending(u => u.Key.IsOnline)
|
||||
.ThenBy(u =>
|
||||
var syncshellFolderTags = new List<IDrawFolder>();
|
||||
foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (string.Equals(u.Key.UserData.UID, group.OwnerUID, StringComparison.Ordinal)) return 0;
|
||||
if (group.GroupPairUserInfos.TryGetValue(u.Key.UserData.UID, out var info))
|
||||
if (_tagHandler.HasSyncshellTag(group.GID, syncshellTag))
|
||||
{
|
||||
if (info.IsModerator()) return 1;
|
||||
if (info.IsPinned()) return 2;
|
||||
GetGroups(allPairs, filteredPairs, group,
|
||||
out ImmutableList<Pair> allGroupPairs,
|
||||
out Dictionary<Pair, List<GroupFullInfoDto>> filteredGroupPairs);
|
||||
|
||||
syncshellFolderTags.Add(_drawEntityFactory.CreateDrawGroupFolder($"tag_{group.GID}", group, filteredGroupPairs, allGroupPairs));
|
||||
}
|
||||
return u.Key.IsVisible ? 3 : 4;
|
||||
})
|
||||
.ThenBy(AlphabeticalSort, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(k => k.Key, k => k.Value);
|
||||
}
|
||||
|
||||
drawFolders.Add(new DrawGroupedGroupFolder(syncshellFolderTags, _tagHandler, _uiSharedService, _selectSyncshellForTagUi, _renameSyncshellTagUi, syncshellTag));
|
||||
}
|
||||
|
||||
//Filter of not grouped/foldered and offline pairs
|
||||
var allOnlineNotTaggedPairs = ImmutablePairList(allPairs.Where(p => FilterNotTaggedUsers(p.Key)));
|
||||
var onlineNotTaggedPairs = BasicSortedDictionary(filteredPairs.Where(p => FilterNotTaggedUsers(p.Key) && FilterOnlineOrPausedSelf(p.Key)));
|
||||
|
||||
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder((_configService.Current.ShowOfflineUsersSeparately ? TagHandler.CustomOnlineTag : TagHandler.CustomAllTag), onlineNotTaggedPairs, allOnlineNotTaggedPairs));
|
||||
|
||||
if (_configService.Current.ShowOfflineUsersSeparately)
|
||||
{
|
||||
var allOfflinePairs = ImmutablePairList(allPairs.Where(p => FilterOfflineUsers(p.Key, p.Value)));
|
||||
var filteredOfflinePairs = BasicSortedDictionary(filteredPairs.Where(p => FilterOfflineUsers(p.Key, p.Value)));
|
||||
|
||||
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineTag, filteredOfflinePairs, allOfflinePairs));
|
||||
|
||||
if (_configService.Current.ShowSyncshellOfflineUsersSeparately)
|
||||
{
|
||||
var allOfflineSyncshellUsers = ImmutablePairList(allPairs.Where(p => FilterOfflineSyncshellUsers(p.Key)));
|
||||
var filteredOfflineSyncshellUsers = BasicSortedDictionary(filteredPairs.Where(p => FilterOfflineSyncshellUsers(p.Key)));
|
||||
|
||||
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomOfflineSyncshellTag, filteredOfflineSyncshellUsers, allOfflineSyncshellUsers));
|
||||
}
|
||||
}
|
||||
|
||||
//Unpaired
|
||||
drawFolders.Add(_drawEntityFactory.CreateDrawTagFolder(TagHandler.CustomUnpairedTag,
|
||||
BasicSortedDictionary(filteredPairs.Where(p => p.Key.IsOneSidedPair)),
|
||||
ImmutablePairList(allPairs.Where(p => p.Key.IsOneSidedPair))));
|
||||
|
||||
return drawFolders;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool PassesFilter(Pair pair, string filter)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filter)) return true;
|
||||
|
||||
return pair.UserData.AliasOrUID.Contains(filter, StringComparison.OrdinalIgnoreCase) || (pair.GetNote()?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false) || (pair.PlayerName?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false);
|
||||
}
|
||||
|
||||
private string AlphabeticalSortKey(Pair pair)
|
||||
{
|
||||
if (_configService.Current.ShowCharacterNameInsteadOfNotesForVisible && !string.IsNullOrEmpty(pair.PlayerName))
|
||||
{
|
||||
return _configService.Current.PreferNotesOverNamesForVisible ? (pair.GetNote() ?? string.Empty) : pair.PlayerName;
|
||||
}
|
||||
|
||||
return pair.GetNote() ?? pair.UserData.AliasOrUID;
|
||||
}
|
||||
|
||||
private bool FilterOnlineOrPausedSelf(Pair pair) => pair.IsOnline || (!pair.IsOnline && !_configService.Current.ShowOfflineUsersSeparately) || pair.UserPair.OwnPermissions.IsPaused();
|
||||
|
||||
private bool FilterVisibleUsers(Pair pair) => pair.IsVisible && (_configService.Current.ShowSyncshellUsersInVisible || pair.IsDirectlyPaired);
|
||||
|
||||
private bool FilterTagUsers(Pair pair, string tag) => pair.IsDirectlyPaired && !pair.IsOneSidedPair && _tagHandler.HasPairTag(pair.UserData.UID, tag);
|
||||
|
||||
private static bool FilterGroupUsers(List<GroupFullInfoDto> groups, GroupFullInfoDto group) => groups.Exists(g => string.Equals(g.GID, group.GID, StringComparison.Ordinal));
|
||||
|
||||
private bool FilterNotTaggedUsers(Pair pair) => pair.IsDirectlyPaired && !pair.IsOneSidedPair && !_tagHandler.HasAnyPairTag(pair.UserData.UID);
|
||||
|
||||
private bool FilterNotTaggedSyncshells(GroupFullInfoDto group) => !_tagHandler.HasAnySyncshellTag(group.GID) || _configService.Current.ShowGroupedSyncshellsInAll;
|
||||
|
||||
private bool FilterOfflineUsers(Pair pair, List<GroupFullInfoDto> groups) => ((pair.IsDirectlyPaired && _configService.Current.ShowSyncshellOfflineUsersSeparately) || !_configService.Current.ShowSyncshellOfflineUsersSeparately) && (!pair.IsOneSidedPair || groups.Count != 0) && !pair.IsOnline && !pair.UserPair.OwnPermissions.IsPaused();
|
||||
|
||||
private static bool FilterOfflineSyncshellUsers(Pair pair) => !pair.IsDirectlyPaired && !pair.IsOnline && !pair.UserPair.OwnPermissions.IsPaused();
|
||||
|
||||
private Dictionary<Pair, List<GroupFullInfoDto>> BasicSortedDictionary(IEnumerable<KeyValuePair<Pair, List<GroupFullInfoDto>>> pairs) => pairs.OrderByDescending(u => u.Key.IsVisible).ThenByDescending(u => u.Key.IsOnline).ThenBy(u => AlphabeticalSortKey(u.Key), StringComparer.OrdinalIgnoreCase).ToDictionary(u => u.Key, u => u.Value);
|
||||
|
||||
private static ImmutableList<Pair> ImmutablePairList(IEnumerable<KeyValuePair<Pair, List<GroupFullInfoDto>>> pairs) => [.. pairs.Select(k => k.Key)];
|
||||
|
||||
private void GetGroups(Dictionary<Pair, List<GroupFullInfoDto>> allPairs,
|
||||
Dictionary<Pair, List<GroupFullInfoDto>> filteredPairs,
|
||||
GroupFullInfoDto group,
|
||||
out ImmutableList<Pair> allGroupPairs,
|
||||
out Dictionary<Pair, List<GroupFullInfoDto>> filteredGroupPairs)
|
||||
{
|
||||
allGroupPairs = ImmutablePairList(allPairs
|
||||
.Where(u => FilterGroupUsers(u.Value, group)));
|
||||
|
||||
filteredGroupPairs = filteredPairs
|
||||
.Where(u => FilterGroupUsers(u.Value, group) && FilterOnlineOrPausedSelf(u.Key))
|
||||
.OrderByDescending(u => u.Key.IsOnline)
|
||||
.ThenBy(u =>
|
||||
{
|
||||
if (string.Equals(u.Key.UserData.UID, group.OwnerUID, StringComparison.Ordinal)) return 0;
|
||||
if (group.GroupPairUserInfos.TryGetValue(u.Key.UserData.UID, out var info))
|
||||
{
|
||||
if (info.IsModerator()) return 1;
|
||||
if (info.IsPinned()) return 2;
|
||||
}
|
||||
return u.Key.IsVisible ? 3 : 4;
|
||||
})
|
||||
.ThenBy(u => AlphabeticalSortKey(u.Key), StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(k => k.Key, k => k.Value);
|
||||
}
|
||||
|
||||
private string GetServerError()
|
||||
{
|
||||
return _apiController.ServerState switch
|
||||
|
||||
@@ -34,8 +34,6 @@ public class DrawGroupedGroupFolder : IDrawFolder
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
if (!_groups.Any()) return;
|
||||
|
||||
string _id = "__folder_syncshells";
|
||||
if (_tag != "")
|
||||
{
|
||||
|
||||
@@ -313,7 +313,7 @@ public class DrawUserPair
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, roleColor))
|
||||
{
|
||||
ImGui.TextUnformatted(_pair.UserData.IsAdmin
|
||||
? "Official Lightless Admin"
|
||||
? "Official Lightless Developer"
|
||||
: "Official Lightless Moderator");
|
||||
}
|
||||
ImGui.EndTooltip();
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Game.Gui.ContextMenu;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.WebAPI;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.UI;
|
||||
|
||||
internal class ContextMenu : IHostedService
|
||||
{
|
||||
private readonly IContextMenu _contextMenu;
|
||||
private readonly IDalamudPluginInterface _pluginInterface;
|
||||
private readonly IDataManager _gameData;
|
||||
private readonly ILogger<ContextMenu> _logger;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly ApiController _apiController;
|
||||
private readonly IObjectTable _objectTable;
|
||||
|
||||
private static readonly string[] ValidAddons = new[]
|
||||
{
|
||||
null,
|
||||
"PartyMemberList", "FriendList", "FreeCompany", "LinkShell", "CrossWorldLinkshell",
|
||||
"_PartyList", "ChatLog", "LookingForGroup", "BlackList", "ContentMemberList",
|
||||
"SocialList", "ContactList", "BeginnerChatList", "MuteList"
|
||||
};
|
||||
|
||||
public ContextMenu(
|
||||
IContextMenu contextMenu,
|
||||
IDalamudPluginInterface pluginInterface,
|
||||
IDataManager gameData,
|
||||
ILogger<ContextMenu> logger,
|
||||
DalamudUtilService dalamudUtil,
|
||||
ApiController apiController,
|
||||
IObjectTable objectTable)
|
||||
{
|
||||
_contextMenu = contextMenu;
|
||||
_pluginInterface = pluginInterface;
|
||||
_gameData = gameData;
|
||||
_logger = logger;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_apiController = apiController;
|
||||
_objectTable = objectTable;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_contextMenu.OnMenuOpened += OnMenuOpened;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_contextMenu.OnMenuOpened -= OnMenuOpened;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Enable()
|
||||
{
|
||||
_contextMenu.OnMenuOpened += OnMenuOpened;
|
||||
_logger.LogDebug("Context menu enabled.");
|
||||
}
|
||||
|
||||
public void Disable()
|
||||
{
|
||||
_contextMenu.OnMenuOpened -= OnMenuOpened;
|
||||
_logger.LogDebug("Context menu disabled.");
|
||||
}
|
||||
|
||||
private void OnMenuOpened(IMenuOpenedArgs args)
|
||||
{
|
||||
if (!_pluginInterface.UiBuilder.ShouldModifyUi)
|
||||
return;
|
||||
|
||||
if (!ValidAddons.Contains(args.AddonName))
|
||||
return;
|
||||
|
||||
if (args.Target is not MenuTargetDefault target)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrEmpty(target.TargetName) || target.TargetObjectId == 0 || target.TargetHomeWorld.RowId == 0)
|
||||
return;
|
||||
|
||||
var world = GetWorld(target.TargetHomeWorld.RowId);
|
||||
if (!IsWorldValid(world))
|
||||
return;
|
||||
|
||||
args.AddMenuItem(new MenuItem
|
||||
{
|
||||
Name = "Send Pair Request",
|
||||
PrefixChar = 'L',
|
||||
UseDefaultPrefix = false,
|
||||
PrefixColor = 708,
|
||||
OnClicked = async _ => await HandleSelection(args)
|
||||
});
|
||||
}
|
||||
|
||||
private async Task HandleSelection(IMenuArgs args)
|
||||
{
|
||||
if (args.Target is not MenuTargetDefault target)
|
||||
return;
|
||||
|
||||
var world = GetWorld(target.TargetHomeWorld.RowId);
|
||||
if (!IsWorldValid(world))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var targetData = _objectTable
|
||||
.OfType<IPlayerCharacter>()
|
||||
.FirstOrDefault(p =>
|
||||
string.Equals(p.Name.TextValue, target.TargetName, StringComparison.OrdinalIgnoreCase) &&
|
||||
p.HomeWorld.RowId == target.TargetHomeWorld.RowId);
|
||||
|
||||
if (targetData == null || targetData.Address == IntPtr.Zero)
|
||||
{
|
||||
_logger.LogWarning("Target player {TargetName}@{World} not found in object table.", target.TargetName, world.Name);
|
||||
return;
|
||||
}
|
||||
|
||||
var senderCid = (await _dalamudUtil.GetCIDAsync()).ToString().GetHash256();
|
||||
var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address);
|
||||
|
||||
_logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid);
|
||||
await _apiController.TryPairWithContentId(receiverCid, senderCid);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error sending pair request.");
|
||||
}
|
||||
}
|
||||
|
||||
private World GetWorld(uint worldId)
|
||||
{
|
||||
var sheet = _gameData.GetExcelSheet<World>()!;
|
||||
return sheet.TryGetRow(worldId, out var world) ? world : sheet.First();
|
||||
}
|
||||
|
||||
public bool IsWorldValid(uint worldId) => IsWorldValid(GetWorld(worldId));
|
||||
|
||||
public static bool IsWorldValid(World world)
|
||||
{
|
||||
var name = world.Name.ToString();
|
||||
return !string.IsNullOrWhiteSpace(name) && char.IsUpper(name[0]);
|
||||
}
|
||||
}
|
||||
@@ -547,73 +547,147 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
using var tab = ImRaii.TabItem(tabText + "###" + kvp.Key.ToString());
|
||||
if (tab.Success)
|
||||
{
|
||||
var groupedfiles = kvp.Value.Select(v => v.Value).GroupBy(f => f.FileType, StringComparer.Ordinal)
|
||||
.OrderBy(k => k.Key, StringComparer.Ordinal).ToList();
|
||||
var groupedfiles = kvp.Value.Select(v => v.Value).GroupBy(f => f.FileType, StringComparer.Ordinal).OrderBy(k => k.Key, StringComparer.Ordinal).ToList();
|
||||
|
||||
ImGui.TextUnformatted("Files for " + kvp.Key);
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(kvp.Value.Count.ToString());
|
||||
ImGui.SameLine();
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.CellPadding, new Vector2(1f, 1f));
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1f, 1f));
|
||||
|
||||
using (var font = ImRaii.PushFont(UiBuilder.IconFont))
|
||||
if (ImGui.BeginTable($"##fileStats_{kvp.Key}", 3,
|
||||
ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingFixedFit))
|
||||
{
|
||||
ImGui.TextUnformatted(FontAwesomeIcon.InfoCircle.ToIconString());
|
||||
}
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
string text = "";
|
||||
text = string.Join(Environment.NewLine, groupedfiles
|
||||
.Select(f => f.Key + ": " + f.Count() + " files, size: " + UiSharedService.ByteToString(f.Sum(v => v.OriginalSize))
|
||||
+ ", compressed: " + UiSharedService.ByteToString(f.Sum(v => v.CompressedSize))));
|
||||
ImGui.SetTooltip(text);
|
||||
}
|
||||
ImGui.TextUnformatted($"{kvp.Key} size (actual):");
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.OriginalSize)));
|
||||
ImGui.TextUnformatted($"{kvp.Key} size (compressed for up/download only):");
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.CompressedSize)));
|
||||
ImGui.Separator();
|
||||
|
||||
var vramUsage = groupedfiles.SingleOrDefault(v => string.Equals(v.Key, "tex", StringComparison.Ordinal));
|
||||
if (vramUsage != null)
|
||||
{
|
||||
var actualVramUsage = vramUsage.Sum(f => f.OriginalSize);
|
||||
ImGui.TextUnformatted($"{kvp.Key} VRAM usage:");
|
||||
ImGui.TableNextRow();
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted($"Files for {kvp.Key}");
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(kvp.Value.Count.ToString());
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(UiSharedService.ByteToString(actualVramUsage));
|
||||
using (var font = ImRaii.PushFont(UiBuilder.IconFont))
|
||||
ImGui.TextUnformatted(FontAwesomeIcon.InfoCircle.ToIconString());
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
string text = string.Join(Environment.NewLine, groupedfiles.Select(f =>
|
||||
$"{f.Key}: {f.Count()} files, size: {UiSharedService.ByteToString(f.Sum(v => v.OriginalSize))}, compressed: {UiSharedService.ByteToString(f.Sum(v => v.CompressedSize))}"));
|
||||
ImGui.SetTooltip(text);
|
||||
}
|
||||
ImGui.TableNextColumn();
|
||||
|
||||
ImGui.TableNextRow();
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted($"{kvp.Key} size (actual):");
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.OriginalSize)));
|
||||
ImGui.TableNextColumn();
|
||||
|
||||
ImGui.TableNextRow();
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted($"{kvp.Key} size (compressed for up/download only):");
|
||||
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.CompressedSize)));
|
||||
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
|
||||
ImGui.TableNextColumn();
|
||||
|
||||
var vramUsage = groupedfiles.SingleOrDefault(v => string.Equals(v.Key, "tex", StringComparison.Ordinal));
|
||||
if (vramUsage != null)
|
||||
{
|
||||
var actualVramUsage = vramUsage.Sum(f => f.OriginalSize);
|
||||
|
||||
ImGui.TableNextRow();
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted($"{kvp.Key} VRAM usage:");
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(UiSharedService.ByteToString(actualVramUsage));
|
||||
ImGui.TableNextColumn();
|
||||
|
||||
if (_playerPerformanceConfig.Current.WarnOnExceedingThresholds
|
||||
|| _playerPerformanceConfig.Current.ShowPerformanceIndicator)
|
||||
{
|
||||
var currentVramWarning = _playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB;
|
||||
|
||||
ImGui.TableNextRow();
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted("Configured VRAM threshold:");
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted($"{currentVramWarning} MiB.");
|
||||
ImGui.TableNextColumn();
|
||||
if (currentVramWarning * 1024 * 1024 < actualVramUsage)
|
||||
{
|
||||
UiSharedService.ColorText(
|
||||
$"You exceed your own threshold by {UiSharedService.ByteToString(actualVramUsage - (currentVramWarning * 1024 * 1024))}",
|
||||
UIColors.Get("LightlessYellow"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var actualTriCount = kvp.Value.Sum(f => f.Value.Triangles);
|
||||
ImGui.TableNextRow();
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted($"{kvp.Key} modded model triangles:");
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(actualTriCount.ToString());
|
||||
ImGui.TableNextColumn();
|
||||
|
||||
if (_playerPerformanceConfig.Current.WarnOnExceedingThresholds
|
||||
|| _playerPerformanceConfig.Current.ShowPerformanceIndicator)
|
||||
{
|
||||
using var _ = ImRaii.PushIndent(10f);
|
||||
var currentVramWarning = _playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB;
|
||||
ImGui.TextUnformatted($"Configured VRAM warning threshold: {currentVramWarning} MiB.");
|
||||
if (currentVramWarning * 1024 * 1024 < actualVramUsage)
|
||||
var currentTriWarning = _playerPerformanceConfig.Current.TrisWarningThresholdThousands;
|
||||
|
||||
ImGui.TableNextRow();
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted("Configured triangle threshold:");
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted($"{currentTriWarning * 1000} triangles.");
|
||||
ImGui.TableNextColumn();
|
||||
if (currentTriWarning * 1000 < actualTriCount)
|
||||
{
|
||||
UiSharedService.ColorText($"You exceed your own threshold by " +
|
||||
$"{UiSharedService.ByteToString(actualVramUsage - (currentVramWarning * 1024 * 1024))}.",
|
||||
UiSharedService.ColorText(
|
||||
$"You exceed your own threshold by {actualTriCount - (currentTriWarning * 1000)}",
|
||||
UIColors.Get("LightlessYellow"));
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndTable();
|
||||
}
|
||||
|
||||
var actualTriCount = kvp.Value.Sum(f => f.Value.Triangles);
|
||||
ImGui.TextUnformatted($"{kvp.Key} modded model triangles: {actualTriCount}");
|
||||
if (_playerPerformanceConfig.Current.WarnOnExceedingThresholds
|
||||
|| _playerPerformanceConfig.Current.ShowPerformanceIndicator)
|
||||
ImGui.PopStyleVar(2);
|
||||
|
||||
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
|
||||
|
||||
_uiSharedService.MediumText("Selected file:", UIColors.Get("LightlessBlue"));
|
||||
ImGui.SameLine();
|
||||
_uiSharedService.MediumText(_selectedHash, UIColors.Get("LightlessYellow"));
|
||||
|
||||
if (_cachedAnalysis[_selectedObjectTab].TryGetValue(_selectedHash, out CharacterAnalyzer.FileDataEntry? item))
|
||||
{
|
||||
using var _ = ImRaii.PushIndent(10f);
|
||||
var currentTriWarning = _playerPerformanceConfig.Current.TrisWarningThresholdThousands;
|
||||
ImGui.TextUnformatted($"Configured triangle warning threshold: {currentTriWarning * 1000} triangles.");
|
||||
if (currentTriWarning * 1000 < actualTriCount)
|
||||
var filePaths = item.FilePaths;
|
||||
UiSharedService.ColorText("Local file path:", UIColors.Get("LightlessBlue"));
|
||||
ImGui.SameLine();
|
||||
UiSharedService.TextWrapped(filePaths[0]);
|
||||
if (filePaths.Count > 1)
|
||||
{
|
||||
UiSharedService.ColorText($"You exceed your own threshold by " +
|
||||
$"{actualTriCount - (currentTriWarning * 1000)} triangles.",
|
||||
UIColors.Get("LightlessYellow"));
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted($"(and {filePaths.Count - 1} more)");
|
||||
ImGui.SameLine();
|
||||
_uiSharedService.IconText(FontAwesomeIcon.InfoCircle);
|
||||
UiSharedService.AttachToolTip(string.Join(Environment.NewLine, filePaths.Skip(1)));
|
||||
}
|
||||
|
||||
var gamepaths = item.GamePaths;
|
||||
UiSharedService.ColorText("Used by game path:", UIColors.Get("LightlessBlue"));
|
||||
ImGui.SameLine();
|
||||
UiSharedService.TextWrapped(gamepaths[0]);
|
||||
if (gamepaths.Count > 1)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted($"(and {gamepaths.Count - 1} more)");
|
||||
ImGui.SameLine();
|
||||
_uiSharedService.IconText(FontAwesomeIcon.InfoCircle);
|
||||
UiSharedService.AttachToolTip(string.Join(Environment.NewLine, gamepaths.Skip(1)));
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.Separator();
|
||||
|
||||
if (_selectedObjectTab != kvp.Key)
|
||||
{
|
||||
_selectedHash = string.Empty;
|
||||
@@ -692,41 +766,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.Separator();
|
||||
|
||||
ImGui.TextUnformatted("Selected file:");
|
||||
ImGui.SameLine();
|
||||
UiSharedService.ColorText(_selectedHash, UIColors.Get("LightlessYellow"));
|
||||
|
||||
if (_cachedAnalysis[_selectedObjectTab].TryGetValue(_selectedHash, out CharacterAnalyzer.FileDataEntry? item))
|
||||
{
|
||||
var filePaths = item.FilePaths;
|
||||
ImGui.TextUnformatted("Local file path:");
|
||||
ImGui.SameLine();
|
||||
UiSharedService.TextWrapped(filePaths[0]);
|
||||
if (filePaths.Count > 1)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted($"(and {filePaths.Count - 1} more)");
|
||||
ImGui.SameLine();
|
||||
_uiSharedService.IconText(FontAwesomeIcon.InfoCircle);
|
||||
UiSharedService.AttachToolTip(string.Join(Environment.NewLine, filePaths.Skip(1)));
|
||||
}
|
||||
|
||||
var gamepaths = item.GamePaths;
|
||||
ImGui.TextUnformatted("Used by game path:");
|
||||
ImGui.SameLine();
|
||||
UiSharedService.TextWrapped(gamepaths[0]);
|
||||
if (gamepaths.Count > 1)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted($"(and {gamepaths.Count - 1} more)");
|
||||
ImGui.SameLine();
|
||||
_uiSharedService.IconText(FontAwesomeIcon.InfoCircle);
|
||||
UiSharedService.AttachToolTip(string.Join(Environment.NewLine, gamepaths.Skip(1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnOpen()
|
||||
@@ -855,7 +894,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
var filePath = item.FilePaths[0];
|
||||
bool toConvert = _texturesToConvert.ContainsKey(filePath);
|
||||
if (ImGui.Checkbox("###convert" + item.Hash, ref toConvert))
|
||||
if (UiSharedService.CheckboxWithBorder("###convert" + item.Hash, ref toConvert, UIColors.Get("LightlessPurple"), 1.5f))
|
||||
{
|
||||
if (toConvert && !_texturesToConvert.ContainsKey(filePath))
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Colors;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
@@ -19,16 +20,22 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private readonly FileUploadManager _fileTransferManager;
|
||||
private readonly UiSharedService _uiShared;
|
||||
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||
private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new();
|
||||
private readonly NotificationService _notificationService;
|
||||
private bool _notificationDismissed = true;
|
||||
|
||||
public DownloadUi(ILogger<DownloadUi> logger, DalamudUtilService dalamudUtilService, LightlessConfigService configService,
|
||||
FileUploadManager fileTransferManager, LightlessMediator mediator, UiSharedService uiShared, PerformanceCollectorService performanceCollectorService)
|
||||
PairProcessingLimiter pairProcessingLimiter, FileUploadManager fileTransferManager, LightlessMediator mediator, UiSharedService uiShared,
|
||||
PerformanceCollectorService performanceCollectorService, NotificationService notificationService)
|
||||
: base(logger, mediator, "Lightless Sync Downloads", performanceCollectorService)
|
||||
{
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
_configService = configService;
|
||||
_pairProcessingLimiter = pairProcessingLimiter;
|
||||
_fileTransferManager = fileTransferManager;
|
||||
_uiShared = uiShared;
|
||||
_notificationService = notificationService;
|
||||
|
||||
SizeConstraints = new WindowSizeConstraints()
|
||||
{
|
||||
@@ -53,7 +60,14 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
IsOpen = true;
|
||||
|
||||
Mediator.Subscribe<DownloadStartedMessage>(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus);
|
||||
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _));
|
||||
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) =>
|
||||
{
|
||||
_currentDownloads.TryRemove(msg.DownloadId, out _);
|
||||
if (!_currentDownloads.Any())
|
||||
{
|
||||
_notificationService.DismissPairDownloadNotification();
|
||||
}
|
||||
});
|
||||
Mediator.Subscribe<GposeStartMessage>(this, (_) => IsOpen = false);
|
||||
Mediator.Subscribe<GposeEndMessage>(this, (_) => IsOpen = true);
|
||||
Mediator.Subscribe<PlayerUploadingMessage>(this, (msg) =>
|
||||
@@ -73,11 +87,13 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
if (_configService.Current.ShowTransferWindow)
|
||||
{
|
||||
var limiterSnapshot = _pairProcessingLimiter.GetSnapshot();
|
||||
|
||||
try
|
||||
{
|
||||
if (_fileTransferManager.CurrentUploads.Any())
|
||||
if (_fileTransferManager.IsUploading)
|
||||
{
|
||||
var currentUploads = _fileTransferManager.CurrentUploads.ToList();
|
||||
var currentUploads = _fileTransferManager.GetCurrentUploadsSnapshot();
|
||||
var totalUploads = currentUploads.Count;
|
||||
|
||||
var doneUploads = currentUploads.Count(c => c.IsTransferred);
|
||||
@@ -105,28 +121,64 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var item in _currentDownloads.ToList())
|
||||
// Check if download notifications are enabled (not set to TextOverlay)
|
||||
var useNotifications = _configService.Current.UseLightlessNotifications
|
||||
? _configService.Current.LightlessDownloadNotification != NotificationLocation.TextOverlay
|
||||
: _configService.Current.UseNotificationsForDownloads;
|
||||
|
||||
if (useNotifications)
|
||||
{
|
||||
var dlSlot = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForSlot);
|
||||
var dlQueue = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForQueue);
|
||||
var dlProg = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Downloading);
|
||||
var dlDecomp = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Decompressing);
|
||||
var totalFiles = item.Value.Sum(c => c.Value.TotalFiles);
|
||||
var transferredFiles = item.Value.Sum(c => c.Value.TransferredFiles);
|
||||
var totalBytes = item.Value.Sum(c => c.Value.TotalBytes);
|
||||
var transferredBytes = item.Value.Sum(c => c.Value.TransferredBytes);
|
||||
// Use notification system
|
||||
if (_currentDownloads.Any())
|
||||
{
|
||||
UpdateDownloadNotification(limiterSnapshot);
|
||||
_notificationDismissed = false;
|
||||
}
|
||||
else if (!_notificationDismissed)
|
||||
{
|
||||
_notificationService.DismissPairDownloadNotification();
|
||||
_notificationDismissed = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (limiterSnapshot.IsEnabled)
|
||||
{
|
||||
var queueColor = limiterSnapshot.Waiting > 0 ? ImGuiColors.DalamudYellow : ImGuiColors.DalamudGrey;
|
||||
var queueText = $"Pair queue {limiterSnapshot.InFlight}/{limiterSnapshot.Limit}";
|
||||
queueText += limiterSnapshot.Waiting > 0 ? $" ({limiterSnapshot.Waiting} waiting, {limiterSnapshot.Remaining} free)" : $" ({limiterSnapshot.Remaining} free)";
|
||||
UiSharedService.DrawOutlinedFont(queueText, queueColor, new Vector4(0, 0, 0, 255), 1);
|
||||
ImGui.NewLine();
|
||||
}
|
||||
else
|
||||
{
|
||||
UiSharedService.DrawOutlinedFont("Pair apply limiter disabled", ImGuiColors.DalamudGrey, new Vector4(0, 0, 0, 255), 1);
|
||||
ImGui.NewLine();
|
||||
}
|
||||
|
||||
foreach (var item in _currentDownloads.ToList())
|
||||
{
|
||||
var dlSlot = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForSlot);
|
||||
var dlQueue = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForQueue);
|
||||
var dlProg = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Downloading);
|
||||
var dlDecomp = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Decompressing);
|
||||
var totalFiles = item.Value.Sum(c => c.Value.TotalFiles);
|
||||
var transferredFiles = item.Value.Sum(c => c.Value.TransferredFiles);
|
||||
var totalBytes = item.Value.Sum(c => c.Value.TotalBytes);
|
||||
var transferredBytes = item.Value.Sum(c => c.Value.TransferredBytes);
|
||||
|
||||
UiSharedService.DrawOutlinedFont($"▼", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
|
||||
ImGui.SameLine();
|
||||
var xDistance = ImGui.GetCursorPosX();
|
||||
UiSharedService.DrawOutlinedFont(
|
||||
$"{item.Key.Name} [W:{dlSlot}/Q:{dlQueue}/P:{dlProg}/D:{dlDecomp}]",
|
||||
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
|
||||
ImGui.NewLine();
|
||||
ImGui.SameLine(xDistance);
|
||||
UiSharedService.DrawOutlinedFont(
|
||||
$"{transferredFiles}/{totalFiles} ({UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)})",
|
||||
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
|
||||
UiSharedService.DrawOutlinedFont($"▼", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
|
||||
ImGui.SameLine();
|
||||
var xDistance = ImGui.GetCursorPosX();
|
||||
UiSharedService.DrawOutlinedFont(
|
||||
$"{item.Key.Name} [W:{dlSlot}/Q:{dlQueue}/P:{dlProg}/D:{dlDecomp}]",
|
||||
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
|
||||
ImGui.NewLine();
|
||||
ImGui.SameLine(xDistance);
|
||||
UiSharedService.DrawOutlinedFont(
|
||||
$"{transferredFiles}/{totalFiles} ({UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)})",
|
||||
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
@@ -214,7 +266,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
if (_uiShared.EditTrackerPosition) return true;
|
||||
if (!_configService.Current.ShowTransferWindow && !_configService.Current.ShowTransferBars) return false;
|
||||
if (!_currentDownloads.Any() && !_fileTransferManager.CurrentUploads.Any() && !_uploadingPlayers.Any()) return false;
|
||||
if (!_currentDownloads.Any() && !_fileTransferManager.IsUploading && !_uploadingPlayers.Any()) return false;
|
||||
if (!IsOpen) return false;
|
||||
return true;
|
||||
}
|
||||
@@ -245,4 +297,40 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
MaximumSize = new Vector2(300, maxHeight),
|
||||
};
|
||||
}
|
||||
|
||||
private void UpdateDownloadNotification(PairProcessingLimiterSnapshot limiterSnapshot)
|
||||
{
|
||||
var downloadStatus = new List<(string playerName, float progress, string status)>();
|
||||
|
||||
foreach (var item in _currentDownloads.ToList())
|
||||
{
|
||||
var dlSlot = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForSlot);
|
||||
var dlQueue = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForQueue);
|
||||
var dlProg = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Downloading);
|
||||
var dlDecomp = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Decompressing);
|
||||
var totalFiles = item.Value.Sum(c => c.Value.TotalFiles);
|
||||
var transferredFiles = item.Value.Sum(c => c.Value.TransferredFiles);
|
||||
var totalBytes = item.Value.Sum(c => c.Value.TotalBytes);
|
||||
var transferredBytes = item.Value.Sum(c => c.Value.TransferredBytes);
|
||||
|
||||
var progress = totalBytes > 0 ? (float)transferredBytes / totalBytes : 0f;
|
||||
|
||||
string status;
|
||||
if (dlDecomp > 0) status = "decompressing";
|
||||
else if (dlProg > 0) status = "downloading";
|
||||
else if (dlQueue > 0) status = "queued";
|
||||
else if (dlSlot > 0) status = "waiting";
|
||||
else status = "completed";
|
||||
|
||||
downloadStatus.Add((item.Key.Name, progress, status));
|
||||
}
|
||||
|
||||
// Pass queue waiting count separately, show notification if there are downloads or queue items
|
||||
var queueWaiting = limiterSnapshot.IsEnabled ? limiterSnapshot.Waiting : 0;
|
||||
if (downloadStatus.Any() || queueWaiting > 0)
|
||||
{
|
||||
_notificationService.ShowPairDownloadNotification(downloadStatus, queueWaiting);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,56 +1,92 @@
|
||||
using Dalamud.Game.Gui.Dtr;
|
||||
using Dalamud.Game.Gui.Dtr;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Dalamud.Plugin.Services;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.LightlessConfiguration.Configurations;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using LightlessSync.WebAPI;
|
||||
using LightlessSync.WebAPI.SignalR.Utils;
|
||||
using LightlessSync.Utils;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace LightlessSync.UI;
|
||||
|
||||
public sealed class DtrEntry : IDisposable, IHostedService
|
||||
{
|
||||
private static readonly TimeSpan _localHashedCidCacheDuration = TimeSpan.FromMinutes(2);
|
||||
private static readonly TimeSpan _localHashedCidErrorCooldown = TimeSpan.FromMinutes(1);
|
||||
|
||||
private readonly ApiController _apiController;
|
||||
private readonly ServerConfigurationManager _serverManager;
|
||||
private readonly CancellationTokenSource _cancellationTokenSource = new();
|
||||
private readonly ConfigurationServiceBase<LightlessConfig> _configService;
|
||||
private readonly IDtrBar _dtrBar;
|
||||
private readonly Lazy<IDtrBarEntry> _entry;
|
||||
private readonly Lazy<IDtrBarEntry> _statusEntry;
|
||||
private readonly Lazy<IDtrBarEntry> _lightfinderEntry;
|
||||
private readonly ILogger<DtrEntry> _logger;
|
||||
private readonly BroadcastService _broadcastService;
|
||||
private readonly BroadcastScannerService _broadcastScannerService;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly PairRequestService _pairRequestService;
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private Task? _runTask;
|
||||
private string? _text;
|
||||
private string? _tooltip;
|
||||
private Colors _colors;
|
||||
private string? _statusText;
|
||||
private string? _statusTooltip;
|
||||
private Colors _statusColors;
|
||||
private string? _lightfinderText;
|
||||
private string? _lightfinderTooltip;
|
||||
private Colors _lightfinderColors;
|
||||
private string? _localHashedCid;
|
||||
private DateTime _localHashedCidFetchedAt = DateTime.MinValue;
|
||||
private DateTime _localHashedCidNextErrorLog = DateTime.MinValue;
|
||||
private DateTime _pairRequestNextErrorLog = DateTime.MinValue;
|
||||
|
||||
public DtrEntry(ILogger<DtrEntry> logger, IDtrBar dtrBar, ConfigurationServiceBase<LightlessConfig> configService, LightlessMediator lightlessMediator, PairManager pairManager, ApiController apiController, ServerConfigurationManager serverManager)
|
||||
public DtrEntry(
|
||||
ILogger<DtrEntry> logger,
|
||||
IDtrBar dtrBar,
|
||||
ConfigurationServiceBase<LightlessConfig> configService,
|
||||
LightlessMediator lightlessMediator,
|
||||
PairManager pairManager,
|
||||
PairRequestService pairRequestService,
|
||||
ApiController apiController,
|
||||
ServerConfigurationManager serverManager,
|
||||
BroadcastService broadcastService,
|
||||
BroadcastScannerService broadcastScannerService,
|
||||
DalamudUtilService dalamudUtilService)
|
||||
{
|
||||
_logger = logger;
|
||||
_dtrBar = dtrBar;
|
||||
_entry = new(CreateEntry);
|
||||
_statusEntry = new(CreateStatusEntry);
|
||||
_lightfinderEntry = new(CreateLightfinderEntry);
|
||||
_configService = configService;
|
||||
_lightlessMediator = lightlessMediator;
|
||||
_pairManager = pairManager;
|
||||
_pairRequestService = pairRequestService;
|
||||
_apiController = apiController;
|
||||
_serverManager = serverManager;
|
||||
_broadcastService = broadcastService;
|
||||
_broadcastScannerService = broadcastScannerService;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_entry.IsValueCreated)
|
||||
if (_statusEntry.IsValueCreated)
|
||||
{
|
||||
_logger.LogDebug("Disposing DtrEntry");
|
||||
Clear();
|
||||
_entry.Value.Remove();
|
||||
_statusEntry.Value.Remove();
|
||||
}
|
||||
if (_lightfinderEntry.IsValueCreated)
|
||||
_lightfinderEntry.Value.Remove();
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
@@ -70,7 +106,7 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// ignore cancelled
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -80,33 +116,66 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
||||
|
||||
private void Clear()
|
||||
{
|
||||
if (!_entry.IsValueCreated) return;
|
||||
_logger.LogInformation("Clearing entry");
|
||||
_text = null;
|
||||
_tooltip = null;
|
||||
_colors = default;
|
||||
|
||||
_entry.Value.Shown = false;
|
||||
HideStatusEntry();
|
||||
HideLightfinderEntry();
|
||||
}
|
||||
|
||||
private IDtrBarEntry CreateEntry()
|
||||
private void HideStatusEntry()
|
||||
{
|
||||
_logger.LogTrace("Creating new DtrBar entry");
|
||||
if (_statusEntry.IsValueCreated && _statusEntry.Value.Shown)
|
||||
{
|
||||
_logger.LogInformation("Hiding status entry");
|
||||
_statusEntry.Value.Shown = false;
|
||||
}
|
||||
|
||||
_statusText = null;
|
||||
_statusTooltip = null;
|
||||
_statusColors = default;
|
||||
}
|
||||
|
||||
private void HideLightfinderEntry()
|
||||
{
|
||||
if (_lightfinderEntry.IsValueCreated && _lightfinderEntry.Value.Shown)
|
||||
{
|
||||
_logger.LogInformation("Hiding Lightfinder entry");
|
||||
_lightfinderEntry.Value.Shown = false;
|
||||
}
|
||||
|
||||
_lightfinderText = null;
|
||||
_lightfinderTooltip = null;
|
||||
_lightfinderColors = default;
|
||||
}
|
||||
|
||||
private IDtrBarEntry CreateStatusEntry()
|
||||
{
|
||||
_logger.LogTrace("Creating status DtrBar entry");
|
||||
var entry = _dtrBar.Get("Lightless Sync");
|
||||
entry.OnClick = interactionEvent => OnClickEvent(interactionEvent);
|
||||
entry.OnClick = interactionEvent => OnStatusEntryClick(interactionEvent);
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
private void OnClickEvent(DtrInteractionEvent interactionEvent)
|
||||
|
||||
private IDtrBarEntry CreateLightfinderEntry()
|
||||
{
|
||||
if (interactionEvent.ClickType.Equals(MouseClickType.Left) && !interactionEvent.ModifierKeys.Equals(ClickModifierKeys.Shift))
|
||||
_logger.LogTrace("Creating Lightfinder DtrBar entry");
|
||||
var entry = _dtrBar.Get("Lightfinder");
|
||||
entry.OnClick = interactionEvent => OnLightfinderEntryClick(interactionEvent);
|
||||
return entry;
|
||||
}
|
||||
|
||||
private void OnStatusEntryClick(DtrInteractionEvent interactionEvent)
|
||||
{
|
||||
if (interactionEvent.ClickType.Equals(MouseClickType.Left))
|
||||
{
|
||||
_lightlessMediator.Publish(new UiToggleMessage(typeof(CompactUi)));
|
||||
}
|
||||
else if (interactionEvent.ClickType.Equals(MouseClickType.Left) && interactionEvent.ModifierKeys.Equals(ClickModifierKeys.Shift))
|
||||
{
|
||||
_lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
|
||||
if (interactionEvent.ModifierKeys.HasFlag(ClickModifierKeys.Shift))
|
||||
{
|
||||
_lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
|
||||
}
|
||||
else
|
||||
{
|
||||
_lightlessMediator.Publish(new UiToggleMessage(typeof(CompactUi)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (interactionEvent.ClickType.Equals(MouseClickType.Right))
|
||||
@@ -131,6 +200,17 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLightfinderEntryClick(DtrInteractionEvent interactionEvent)
|
||||
{
|
||||
if (!_configService.Current.ShowLightfinderInDtr)
|
||||
return;
|
||||
|
||||
if (interactionEvent.ClickType.Equals(MouseClickType.Left))
|
||||
{
|
||||
_broadcastService.ToggleBroadcast();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunAsync()
|
||||
{
|
||||
while (!_cancellationTokenSource.IsCancellationRequested)
|
||||
@@ -143,96 +223,278 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!_configService.Current.EnableDtrEntry || !_configService.Current.HasValidSetup())
|
||||
{
|
||||
if (_entry.IsValueCreated && _entry.Value.Shown)
|
||||
{
|
||||
_logger.LogInformation("Disabling entry");
|
||||
var config = _configService.Current;
|
||||
|
||||
Clear();
|
||||
}
|
||||
if (!config.HasValidSetup())
|
||||
{
|
||||
HideStatusEntry();
|
||||
HideLightfinderEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_entry.Value.Shown)
|
||||
{
|
||||
_logger.LogInformation("Showing entry");
|
||||
_entry.Value.Shown = true;
|
||||
}
|
||||
if (config.EnableDtrEntry)
|
||||
UpdateStatusEntry(config);
|
||||
else
|
||||
HideStatusEntry();
|
||||
|
||||
if (config.ShowLightfinderInDtr)
|
||||
UpdateLightfinderEntry(config);
|
||||
else
|
||||
HideLightfinderEntry();
|
||||
}
|
||||
|
||||
private void UpdateStatusEntry(LightlessConfig config)
|
||||
{
|
||||
string text;
|
||||
string tooltip;
|
||||
Colors colors;
|
||||
|
||||
if (_apiController.IsConnected)
|
||||
{
|
||||
var pairCount = _pairManager.GetVisibleUserCount();
|
||||
text = $"\uE044 {pairCount}";
|
||||
if (pairCount > 0)
|
||||
{
|
||||
IEnumerable<string> visiblePairs;
|
||||
if (_configService.Current.ShowUidInDtrTooltip)
|
||||
{
|
||||
visiblePairs = _pairManager.GetOnlineUserPairs()
|
||||
.Where(x => x.IsVisible)
|
||||
.Select(x => string.Format("{0} ({1})", _configService.Current.PreferNoteInDtrTooltip ? x.GetNote() ?? x.PlayerName : x.PlayerName, x.UserData.AliasOrUID));
|
||||
}
|
||||
else
|
||||
{
|
||||
visiblePairs = _pairManager.GetOnlineUserPairs()
|
||||
.Where(x => x.IsVisible)
|
||||
.Select(x => string.Format("{0}", _configService.Current.PreferNoteInDtrTooltip ? x.GetNote() ?? x.PlayerName : x.PlayerName));
|
||||
}
|
||||
var preferNote = config.PreferNoteInDtrTooltip;
|
||||
var showUid = config.ShowUidInDtrTooltip;
|
||||
|
||||
var visiblePairsQuery = _pairManager.GetOnlineUserPairs()
|
||||
.Where(x => x.IsVisible);
|
||||
|
||||
IEnumerable<string> visiblePairs = showUid
|
||||
? visiblePairsQuery.Select(x => string.Format("{0} ({1})", preferNote ? x.GetNote() ?? x.PlayerName : x.PlayerName, x.UserData.AliasOrUID))
|
||||
: visiblePairsQuery.Select(x => string.Format("{0}", preferNote ? x.GetNote() ?? x.PlayerName : x.PlayerName));
|
||||
|
||||
tooltip = $"Lightless Sync: Connected{Environment.NewLine}----------{Environment.NewLine}{string.Join(Environment.NewLine, visiblePairs)}";
|
||||
colors = _configService.Current.DtrColorsPairsInRange;
|
||||
colors = config.DtrColorsPairsInRange;
|
||||
}
|
||||
else
|
||||
{
|
||||
tooltip = "Lightless Sync: Connected";
|
||||
colors = _configService.Current.DtrColorsDefault;
|
||||
colors = config.DtrColorsDefault;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
text = "\uE044 \uE04C";
|
||||
tooltip = "Lightless Sync: Not Connected";
|
||||
colors = _configService.Current.DtrColorsNotConnected;
|
||||
colors = config.DtrColorsNotConnected;
|
||||
}
|
||||
|
||||
if (!_configService.Current.UseColorsInDtr)
|
||||
if (!config.UseColorsInDtr)
|
||||
colors = default;
|
||||
|
||||
if (!string.Equals(text, _text, StringComparison.Ordinal) || !string.Equals(tooltip, _tooltip, StringComparison.Ordinal) || colors != _colors)
|
||||
var statusEntry = _statusEntry.Value;
|
||||
if (!statusEntry.Shown)
|
||||
{
|
||||
_text = text;
|
||||
_tooltip = tooltip;
|
||||
_colors = colors;
|
||||
_entry.Value.Text = BuildColoredSeString(text, colors);
|
||||
_entry.Value.Tooltip = tooltip;
|
||||
_logger.LogInformation("Showing status entry");
|
||||
statusEntry.Shown = true;
|
||||
}
|
||||
|
||||
bool statusNeedsUpdate =
|
||||
!string.Equals(text, _statusText, StringComparison.Ordinal) ||
|
||||
!string.Equals(tooltip, _statusTooltip, StringComparison.Ordinal) ||
|
||||
colors != _statusColors;
|
||||
|
||||
if (statusNeedsUpdate)
|
||||
{
|
||||
statusEntry.Text = BuildColoredSeString(text, colors);
|
||||
statusEntry.Tooltip = tooltip;
|
||||
_statusText = text;
|
||||
_statusTooltip = tooltip;
|
||||
_statusColors = colors;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateLightfinderEntry(LightlessConfig config)
|
||||
{
|
||||
var lightfinderEntry = _lightfinderEntry.Value;
|
||||
if (!lightfinderEntry.Shown)
|
||||
{
|
||||
_logger.LogInformation("Showing Lightfinder entry");
|
||||
lightfinderEntry.Shown = true;
|
||||
}
|
||||
|
||||
var indicator = BuildLightfinderIndicator();
|
||||
var lightfinderText = indicator.Text ?? string.Empty;
|
||||
var lightfinderColors = config.UseLightfinderColorsInDtr ? indicator.Colors : default;
|
||||
var lightfinderTooltip = BuildLightfinderTooltip(indicator.Tooltip);
|
||||
|
||||
bool lightfinderNeedsUpdate =
|
||||
!string.Equals(lightfinderText, _lightfinderText, StringComparison.Ordinal) ||
|
||||
!string.Equals(lightfinderTooltip, _lightfinderTooltip, StringComparison.Ordinal) ||
|
||||
lightfinderColors != _lightfinderColors;
|
||||
|
||||
if (lightfinderNeedsUpdate)
|
||||
{
|
||||
lightfinderEntry.Text = BuildColoredSeString(lightfinderText, lightfinderColors);
|
||||
lightfinderEntry.Tooltip = lightfinderTooltip;
|
||||
_lightfinderText = lightfinderText;
|
||||
_lightfinderTooltip = lightfinderTooltip;
|
||||
_lightfinderColors = lightfinderColors;
|
||||
}
|
||||
}
|
||||
|
||||
private string? GetLocalHashedCid()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
if (_localHashedCid is not null && now - _localHashedCidFetchedAt < _localHashedCidCacheDuration)
|
||||
return _localHashedCid;
|
||||
|
||||
try
|
||||
{
|
||||
var cid = _dalamudUtilService.GetCIDAsync().GetAwaiter().GetResult();
|
||||
var hashedCid = cid.ToString().GetHash256();
|
||||
_localHashedCid = hashedCid;
|
||||
_localHashedCidFetchedAt = now;
|
||||
return hashedCid;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (now >= _localHashedCidNextErrorLog)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to refresh local hashed CID for Lightfinder DTR entry.");
|
||||
_localHashedCidNextErrorLog = now + _localHashedCidErrorCooldown;
|
||||
}
|
||||
|
||||
_localHashedCid = null;
|
||||
_localHashedCidFetchedAt = now;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private int GetNearbyBroadcastCount()
|
||||
{
|
||||
var localHashedCid = GetLocalHashedCid();
|
||||
return _broadcastScannerService.CountActiveBroadcasts(
|
||||
string.IsNullOrEmpty(localHashedCid) ? null : localHashedCid);
|
||||
}
|
||||
|
||||
private int GetPendingPairRequestCount()
|
||||
{
|
||||
try
|
||||
{
|
||||
return _pairRequestService.GetActiveRequests().Count;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
if (now >= _pairRequestNextErrorLog)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to retrieve pair request count for Lightfinder DTR entry.");
|
||||
_pairRequestNextErrorLog = now + _localHashedCidErrorCooldown;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private (string Text, Colors Colors, string Tooltip) BuildLightfinderIndicator()
|
||||
{
|
||||
var config = _configService.Current;
|
||||
const string icon = "\uE048";
|
||||
if (!_broadcastService.IsLightFinderAvailable)
|
||||
{
|
||||
return ($"{icon} --", SwapColorChannels(config.DtrColorsLightfinderUnavailable), "Lightfinder - Unavailable on this server.");
|
||||
}
|
||||
|
||||
if (_broadcastService.IsBroadcasting)
|
||||
{
|
||||
var tooltipBuilder = new StringBuilder("Lightfinder - Enabled");
|
||||
|
||||
switch (config.LightfinderDtrDisplayMode)
|
||||
{
|
||||
case LightfinderDtrDisplayMode.PendingPairRequests:
|
||||
{
|
||||
var requestCount = GetPendingPairRequestCount();
|
||||
tooltipBuilder.AppendLine();
|
||||
tooltipBuilder.Append("Pending pair requests: ").Append(requestCount);
|
||||
return ($"{icon} Requests {requestCount}", SwapColorChannels(config.DtrColorsLightfinderEnabled), tooltipBuilder.ToString());
|
||||
}
|
||||
default:
|
||||
{
|
||||
var broadcastCount = GetNearbyBroadcastCount();
|
||||
tooltipBuilder.AppendLine();
|
||||
tooltipBuilder.Append("Nearby Lightfinder users: ").Append(broadcastCount);
|
||||
return ($"{icon} {broadcastCount}", SwapColorChannels(config.DtrColorsLightfinderEnabled), tooltipBuilder.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var tooltip = new StringBuilder("Lightfinder - Disabled");
|
||||
var colors = SwapColorChannels(config.DtrColorsLightfinderDisabled);
|
||||
if (_broadcastService.RemainingCooldown is { } cooldown && cooldown > TimeSpan.Zero)
|
||||
{
|
||||
tooltip.AppendLine();
|
||||
tooltip.Append("Cooldown: ").Append(Math.Ceiling(cooldown.TotalSeconds)).Append("s");
|
||||
colors = SwapColorChannels(config.DtrColorsLightfinderCooldown);
|
||||
}
|
||||
|
||||
return ($"{icon} OFF", colors, tooltip.ToString());
|
||||
}
|
||||
|
||||
private static string BuildLightfinderTooltip(string baseTooltip)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
if (!string.IsNullOrWhiteSpace(baseTooltip))
|
||||
builder.Append(baseTooltip.TrimEnd());
|
||||
else
|
||||
builder.Append("Lightfinder status unavailable.");
|
||||
|
||||
return builder.ToString().TrimEnd();
|
||||
}
|
||||
|
||||
private static void AppendColoredSegment(SeStringBuilder builder, string? text, Colors colors)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return;
|
||||
|
||||
if (colors.Foreground != default)
|
||||
builder.Add(BuildColorStartPayload(_colorTypeForeground, colors.Foreground));
|
||||
if (colors.Glow != default)
|
||||
builder.Add(BuildColorStartPayload(_colorTypeGlow, colors.Glow));
|
||||
|
||||
builder.AddText(text);
|
||||
|
||||
if (colors.Glow != default)
|
||||
builder.Add(BuildColorEndPayload(_colorTypeGlow));
|
||||
if (colors.Foreground != default)
|
||||
builder.Add(BuildColorEndPayload(_colorTypeForeground));
|
||||
}
|
||||
|
||||
#region Colored SeString
|
||||
private const byte _colorTypeForeground = 0x13;
|
||||
private const byte _colorTypeGlow = 0x14;
|
||||
|
||||
private static Colors SwapColorChannels(Colors colors)
|
||||
=> new(SwapColorComponent(colors.Foreground), SwapColorComponent(colors.Glow));
|
||||
|
||||
private static uint SwapColorComponent(uint color)
|
||||
{
|
||||
if (color == 0)
|
||||
return 0;
|
||||
|
||||
return ((color & 0xFFu) << 16) | (color & 0xFF00u) | ((color >> 16) & 0xFFu);
|
||||
}
|
||||
|
||||
private static SeString BuildColoredSeString(string text, Colors colors)
|
||||
{
|
||||
var ssb = new SeStringBuilder();
|
||||
if (colors.Foreground != default)
|
||||
ssb.Add(BuildColorStartPayload(_colorTypeForeground, colors.Foreground));
|
||||
if (colors.Glow != default)
|
||||
ssb.Add(BuildColorStartPayload(_colorTypeGlow, colors.Glow));
|
||||
ssb.AddText(text);
|
||||
if (colors.Glow != default)
|
||||
ssb.Add(BuildColorEndPayload(_colorTypeGlow));
|
||||
if (colors.Foreground != default)
|
||||
ssb.Add(BuildColorEndPayload(_colorTypeForeground));
|
||||
AppendColoredSegment(ssb, text, colors);
|
||||
return ssb.Build();
|
||||
}
|
||||
|
||||
private static RawPayload BuildColorStartPayload(byte colorType, uint color)
|
||||
=> new(unchecked([0x02, colorType, 0x05, 0xF6, byte.Max((byte)color, 0x01), byte.Max((byte)(color >> 8), 0x01), byte.Max((byte)(color >> 16), 0x01), 0x03]));
|
||||
=> new(unchecked([
|
||||
0x02,
|
||||
colorType,
|
||||
0x05,
|
||||
0xF6,
|
||||
byte.Max((byte)color, (byte)0x01),
|
||||
byte.Max((byte)(color >> 8), (byte)0x01),
|
||||
byte.Max((byte)(color >> 16), (byte)0x01),
|
||||
0x03
|
||||
]));
|
||||
|
||||
private static RawPayload BuildColorEndPayload(byte colorType)
|
||||
=> new([0x02, colorType, 0x02, 0xEC, 0x03]);
|
||||
|
||||
@@ -4,14 +4,18 @@ using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.ImGuiFileDialog;
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI.Style;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.WebAPI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System.Numerics;
|
||||
|
||||
namespace LightlessSync.UI;
|
||||
|
||||
@@ -30,6 +34,16 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
||||
private bool _showFileDialogError = false;
|
||||
private bool _wasOpen;
|
||||
|
||||
private Vector4 _currentBg = new(0.15f, 0.15f, 0.15f, 1f);
|
||||
private bool vanityInitialized; // useless for now
|
||||
private bool textEnabled;
|
||||
private bool glowEnabled;
|
||||
private Vector4 textColor;
|
||||
private Vector4 glowColor;
|
||||
|
||||
private record VanityState(bool TextEnabled, bool GlowEnabled, Vector4 TextColor, Vector4 GlowColor);
|
||||
private VanityState _savedVanity;
|
||||
|
||||
public EditProfileUi(ILogger<EditProfileUi> logger, LightlessMediator mediator,
|
||||
ApiController apiController, UiSharedService uiSharedService, FileDialogManager fileDialogManager,
|
||||
LightlessProfileManager lightlessProfileManager, PerformanceCollectorService performanceCollectorService)
|
||||
@@ -38,8 +52,8 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
||||
IsOpen = false;
|
||||
this.SizeConstraints = new()
|
||||
{
|
||||
MinimumSize = new(768, 512),
|
||||
MaximumSize = new(768, 2000)
|
||||
MinimumSize = new(850, 640),
|
||||
MaximumSize = new(850, 700)
|
||||
};
|
||||
_apiController = apiController;
|
||||
_uiSharedService = uiSharedService;
|
||||
@@ -57,172 +71,320 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
||||
_pfpTextureWrap = null;
|
||||
}
|
||||
});
|
||||
Mediator.Subscribe<ConnectedMessage>(this, msg =>
|
||||
{
|
||||
LoadVanity();
|
||||
});
|
||||
}
|
||||
|
||||
private void LoadVanity()
|
||||
{
|
||||
textEnabled = !string.IsNullOrEmpty(_apiController.TextColorHex);
|
||||
glowEnabled = !string.IsNullOrEmpty(_apiController.TextGlowColorHex);
|
||||
|
||||
textColor = textEnabled ? UIColors.HexToRgba(_apiController.TextColorHex!) : Vector4.One;
|
||||
glowColor = glowEnabled ? UIColors.HexToRgba(_apiController.TextGlowColorHex!) : Vector4.Zero;
|
||||
|
||||
_savedVanity = new VanityState(textEnabled, glowEnabled, textColor, glowColor);
|
||||
vanityInitialized = true;
|
||||
}
|
||||
|
||||
protected override void DrawInternal()
|
||||
{
|
||||
_uiSharedService.BigText("Current Profile (as saved on server)");
|
||||
_uiSharedService.UnderlinedBigText("Notes and Rules for Profiles", UIColors.Get("LightlessYellow"));
|
||||
ImGui.Dummy(new Vector2(5));
|
||||
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1, 1));
|
||||
|
||||
_uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessBlue"), "All users that are paired and unpaused with you will be able to see your profile picture and description.");
|
||||
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "Other users have the possibility to report your profile for breaking the rules.");
|
||||
_uiSharedService.DrawNoteLine("!!! ", UIColors.Get("DimRed"), "AVOID: Anything as profile image that can be considered highly illegal or obscene (bestiality, anything that could be considered a sexual act with a minor (that includes Lalafells), etc.)");
|
||||
_uiSharedService.DrawNoteLine("!!! ", UIColors.Get("DimRed"), "AVOID: Slurs of any kind in the description that can be considered highly offensive");
|
||||
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "In case of valid reports from other users this can lead to disabling your profile forever or terminating your Lightless account indefinitely.");
|
||||
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), "Judgement of your profile validity from reports through staff is not up to debate and the decisions to disable your profile/account permanent.");
|
||||
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessBlue"), "If your profile picture or profile description could be considered NSFW, enable the toggle in profile settings.");
|
||||
|
||||
ImGui.PopStyleVar();
|
||||
|
||||
ImGui.Dummy(new Vector2(3));
|
||||
|
||||
var profile = _lightlessProfileManager.GetLightlessProfile(new UserData(_apiController.UID));
|
||||
|
||||
if (profile.IsFlagged)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped(profile.Description, ImGuiColors.DalamudRed);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_profileImage.SequenceEqual(profile.ImageData.Value))
|
||||
{
|
||||
_profileImage = profile.ImageData.Value;
|
||||
_pfpTextureWrap?.Dispose();
|
||||
_pfpTextureWrap = _uiSharedService.LoadImage(_profileImage);
|
||||
}
|
||||
|
||||
if (!string.Equals(_profileDescription, profile.Description, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_profileDescription = profile.Description;
|
||||
_descriptionText = _profileDescription;
|
||||
}
|
||||
|
||||
if (_pfpTextureWrap != null)
|
||||
{
|
||||
ImGui.Image(_pfpTextureWrap.Handle, ImGuiHelpers.ScaledVector2(_pfpTextureWrap.Width, _pfpTextureWrap.Height));
|
||||
}
|
||||
|
||||
var spacing = ImGui.GetStyle().ItemSpacing.X;
|
||||
ImGuiHelpers.ScaledRelativeSameLine(256, spacing);
|
||||
using (_uiSharedService.GameFont.Push())
|
||||
{
|
||||
var descriptionTextSize = ImGui.CalcTextSize(profile.Description, wrapWidth: 256f);
|
||||
var childFrame = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 256);
|
||||
if (descriptionTextSize.Y > childFrame.Y)
|
||||
if (ImGui.BeginTabBar("##EditProfileTabs"))
|
||||
{
|
||||
if (ImGui.BeginTabItem("Current Profile"))
|
||||
{
|
||||
_adjustedForScollBarsOnlineProfile = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_adjustedForScollBarsOnlineProfile = false;
|
||||
}
|
||||
childFrame = childFrame with
|
||||
{
|
||||
X = childFrame.X + (_adjustedForScollBarsOnlineProfile ? ImGui.GetStyle().ScrollbarSize : 0),
|
||||
};
|
||||
if (ImGui.BeginChildFrame(101, childFrame))
|
||||
{
|
||||
UiSharedService.TextWrapped(profile.Description);
|
||||
}
|
||||
ImGui.EndChildFrame();
|
||||
}
|
||||
_uiSharedService.MediumText("Current Profile (as saved on server)", UIColors.Get("LightlessPurple"));
|
||||
ImGui.Dummy(new Vector2(5));
|
||||
|
||||
var nsfw = profile.IsNSFW;
|
||||
ImGui.BeginDisabled();
|
||||
ImGui.Checkbox("Is NSFW", ref nsfw);
|
||||
ImGui.EndDisabled();
|
||||
|
||||
ImGui.Separator();
|
||||
_uiSharedService.BigText("Notes and Rules for Profiles");
|
||||
|
||||
ImGui.TextWrapped($"- All users that are paired and unpaused with you will be able to see your profile picture and description.{Environment.NewLine}" +
|
||||
$"- Other users have the possibility to report your profile for breaking the rules.{Environment.NewLine}" +
|
||||
$"- !!! AVOID: anything as profile image that can be considered highly illegal or obscene (bestiality, anything that could be considered a sexual act with a minor (that includes Lalafells), etc.){Environment.NewLine}" +
|
||||
$"- !!! AVOID: slurs of any kind in the description that can be considered highly offensive{Environment.NewLine}" +
|
||||
$"- In case of valid reports from other users this can lead to disabling your profile forever or terminating your Lightless account indefinitely.{Environment.NewLine}" +
|
||||
$"- Judgement of your profile validity from reports through staff is not up to debate and the decisions to disable your profile/account permanent.{Environment.NewLine}" +
|
||||
$"- If your profile picture or profile description could be considered NSFW, enable the toggle below.");
|
||||
ImGui.Separator();
|
||||
_uiSharedService.BigText("Profile Settings");
|
||||
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture"))
|
||||
{
|
||||
_fileDialogManager.OpenFileDialog("Select new Profile picture", ".png", (success, file) =>
|
||||
{
|
||||
if (!success) return;
|
||||
_ = Task.Run(async () =>
|
||||
if (profile.IsFlagged)
|
||||
{
|
||||
var fileContent = File.ReadAllBytes(file);
|
||||
using MemoryStream ms = new(fileContent);
|
||||
var format = await Image.DetectFormatAsync(ms).ConfigureAwait(false);
|
||||
if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase))
|
||||
UiSharedService.ColorTextWrapped(profile.Description, ImGuiColors.DalamudRed);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_profileImage.SequenceEqual(profile.ImageData.Value))
|
||||
{
|
||||
_profileImage = profile.ImageData.Value;
|
||||
_pfpTextureWrap?.Dispose();
|
||||
_pfpTextureWrap = _uiSharedService.LoadImage(_profileImage);
|
||||
}
|
||||
|
||||
if (!string.Equals(_profileDescription, profile.Description, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_profileDescription = profile.Description;
|
||||
_descriptionText = _profileDescription;
|
||||
}
|
||||
|
||||
if (_pfpTextureWrap != null)
|
||||
{
|
||||
ImGui.Image(_pfpTextureWrap.Handle, ImGuiHelpers.ScaledVector2(_pfpTextureWrap.Width, _pfpTextureWrap.Height));
|
||||
}
|
||||
|
||||
var spacing = ImGui.GetStyle().ItemSpacing.X;
|
||||
ImGuiHelpers.ScaledRelativeSameLine(256, spacing);
|
||||
using (_uiSharedService.GameFont.Push())
|
||||
{
|
||||
var descriptionTextSize = ImGui.CalcTextSize(profile.Description, wrapWidth: 256f);
|
||||
var childFrame = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 256);
|
||||
if (descriptionTextSize.Y > childFrame.Y)
|
||||
{
|
||||
_showFileDialogError = true;
|
||||
return;
|
||||
_adjustedForScollBarsOnlineProfile = true;
|
||||
}
|
||||
using var image = Image.Load<Rgba32>(fileContent);
|
||||
|
||||
if (image.Width > 256 || image.Height > 256 || (fileContent.Length > 250 * 1024))
|
||||
else
|
||||
{
|
||||
_showFileDialogError = true;
|
||||
return;
|
||||
_adjustedForScollBarsOnlineProfile = false;
|
||||
}
|
||||
childFrame = childFrame with
|
||||
{
|
||||
X = childFrame.X + (_adjustedForScollBarsOnlineProfile ? ImGui.GetStyle().ScrollbarSize : 0),
|
||||
};
|
||||
if (ImGui.BeginChildFrame(101, childFrame))
|
||||
{
|
||||
UiSharedService.TextWrapped(profile.Description);
|
||||
}
|
||||
ImGui.EndChildFrame();
|
||||
}
|
||||
|
||||
_showFileDialogError = false;
|
||||
await _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, Convert.ToBase64String(fileContent), Description: null))
|
||||
.ConfigureAwait(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
UiSharedService.AttachToolTip("Select and upload a new profile picture");
|
||||
ImGui.SameLine();
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear uploaded profile picture"))
|
||||
{
|
||||
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, "", Description: null));
|
||||
}
|
||||
UiSharedService.AttachToolTip("Clear your currently uploaded profile picture");
|
||||
if (_showFileDialogError)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped("The profile picture must be a PNG file with a maximum height and width of 256px and 250KiB size", ImGuiColors.DalamudRed);
|
||||
}
|
||||
var isNsfw = profile.IsNSFW;
|
||||
if (ImGui.Checkbox("Profile is NSFW", ref isNsfw))
|
||||
{
|
||||
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, isNsfw, ProfilePictureBase64: null, Description: null));
|
||||
}
|
||||
_uiSharedService.DrawHelpText("If your profile description or image can be considered NSFW, toggle this to ON");
|
||||
var widthTextBox = 400;
|
||||
var posX = ImGui.GetCursorPosX();
|
||||
ImGui.TextUnformatted($"Description {_descriptionText.Length}/1500");
|
||||
ImGui.SetCursorPosX(posX);
|
||||
ImGuiHelpers.ScaledRelativeSameLine(widthTextBox, ImGui.GetStyle().ItemSpacing.X);
|
||||
ImGui.TextUnformatted("Preview (approximate)");
|
||||
using (_uiSharedService.GameFont.Push())
|
||||
ImGui.InputTextMultiline("##description", ref _descriptionText, 1500, ImGuiHelpers.ScaledVector2(widthTextBox, 200));
|
||||
var nsfw = profile.IsNSFW;
|
||||
ImGui.BeginDisabled();
|
||||
ImGui.Checkbox("Is NSFW", ref nsfw);
|
||||
ImGui.EndDisabled();
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
using (_uiSharedService.GameFont.Push())
|
||||
{
|
||||
var descriptionTextSizeLocal = ImGui.CalcTextSize(_descriptionText, wrapWidth: 256f);
|
||||
var childFrameLocal = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 200);
|
||||
if (descriptionTextSizeLocal.Y > childFrameLocal.Y)
|
||||
{
|
||||
_adjustedForScollBarsLocalProfile = true;
|
||||
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
else
|
||||
{
|
||||
_adjustedForScollBarsLocalProfile = false;
|
||||
}
|
||||
childFrameLocal = childFrameLocal with
|
||||
{
|
||||
X = childFrameLocal.X + (_adjustedForScollBarsLocalProfile ? ImGui.GetStyle().ScrollbarSize : 0),
|
||||
};
|
||||
if (ImGui.BeginChildFrame(102, childFrameLocal))
|
||||
{
|
||||
UiSharedService.TextWrapped(_descriptionText);
|
||||
}
|
||||
ImGui.EndChildFrame();
|
||||
}
|
||||
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Description"))
|
||||
{
|
||||
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, _descriptionText));
|
||||
if (ImGui.BeginTabItem("Profile Settings"))
|
||||
{
|
||||
_uiSharedService.MediumText("Profile Settings", UIColors.Get("LightlessPurple"));
|
||||
ImGui.Dummy(new Vector2(5));
|
||||
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture"))
|
||||
{
|
||||
_fileDialogManager.OpenFileDialog("Select new Profile picture", ".png", (success, file) =>
|
||||
{
|
||||
if (!success) return;
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
var fileContent = File.ReadAllBytes(file);
|
||||
using MemoryStream ms = new(fileContent);
|
||||
var format = await Image.DetectFormatAsync(ms).ConfigureAwait(false);
|
||||
if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
_showFileDialogError = true;
|
||||
return;
|
||||
}
|
||||
using var image = Image.Load<Rgba32>(fileContent);
|
||||
|
||||
if (image.Width > 256 || image.Height > 256 || (fileContent.Length > 250 * 1024))
|
||||
{
|
||||
_showFileDialogError = true;
|
||||
return;
|
||||
}
|
||||
|
||||
_showFileDialogError = false;
|
||||
await _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, Convert.ToBase64String(fileContent), Description: null))
|
||||
.ConfigureAwait(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
UiSharedService.AttachToolTip("Select and upload a new profile picture");
|
||||
ImGui.SameLine();
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear uploaded profile picture"))
|
||||
{
|
||||
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, "", Description: null));
|
||||
}
|
||||
UiSharedService.AttachToolTip("Clear your currently uploaded profile picture");
|
||||
if (_showFileDialogError)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped("The profile picture must be a PNG file with a maximum height and width of 256px and 250KiB size", ImGuiColors.DalamudRed);
|
||||
}
|
||||
var isNsfw = profile.IsNSFW;
|
||||
if (ImGui.Checkbox("Profile is NSFW", ref isNsfw))
|
||||
{
|
||||
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, isNsfw, ProfilePictureBase64: null, Description: null));
|
||||
}
|
||||
_uiSharedService.DrawHelpText("If your profile description or image can be considered NSFW, toggle this to ON");
|
||||
var widthTextBox = 400;
|
||||
var posX = ImGui.GetCursorPosX();
|
||||
ImGui.TextUnformatted($"Description {_descriptionText.Length}/1500");
|
||||
ImGui.SetCursorPosX(posX);
|
||||
ImGuiHelpers.ScaledRelativeSameLine(widthTextBox, ImGui.GetStyle().ItemSpacing.X);
|
||||
ImGui.TextUnformatted("Preview (approximate)");
|
||||
using (_uiSharedService.GameFont.Push())
|
||||
ImGui.InputTextMultiline("##description", ref _descriptionText, 1500, ImGuiHelpers.ScaledVector2(widthTextBox, 200));
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
using (_uiSharedService.GameFont.Push())
|
||||
{
|
||||
var descriptionTextSizeLocal = ImGui.CalcTextSize(_descriptionText, wrapWidth: 256f);
|
||||
var childFrameLocal = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 200);
|
||||
if (descriptionTextSizeLocal.Y > childFrameLocal.Y)
|
||||
{
|
||||
_adjustedForScollBarsLocalProfile = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_adjustedForScollBarsLocalProfile = false;
|
||||
}
|
||||
childFrameLocal = childFrameLocal with
|
||||
{
|
||||
X = childFrameLocal.X + (_adjustedForScollBarsLocalProfile ? ImGui.GetStyle().ScrollbarSize : 0),
|
||||
};
|
||||
if (ImGui.BeginChildFrame(102, childFrameLocal))
|
||||
{
|
||||
UiSharedService.TextWrapped(_descriptionText);
|
||||
}
|
||||
ImGui.EndChildFrame();
|
||||
}
|
||||
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Description"))
|
||||
{
|
||||
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, _descriptionText));
|
||||
}
|
||||
UiSharedService.AttachToolTip("Sets your profile description text");
|
||||
ImGui.SameLine();
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear Description"))
|
||||
{
|
||||
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, ""));
|
||||
}
|
||||
UiSharedService.AttachToolTip("Clears your profile description text");
|
||||
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
|
||||
if (ImGui.BeginTabItem("Vanity Settings"))
|
||||
{
|
||||
_uiSharedService.MediumText("Supporter Vanity Settings", UIColors.Get("LightlessPurple"));
|
||||
ImGui.Dummy(new Vector2(4));
|
||||
_uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "Must be a supporter through Patreon/Ko-fi to access these settings.");
|
||||
|
||||
var hasVanity = _apiController.HasVanity;
|
||||
|
||||
if (!hasVanity)
|
||||
{
|
||||
UiSharedService.ColorTextWrapped("You do not currently have vanity access. Become a supporter to unlock these features.", UIColors.Get("DimRed"));
|
||||
ImGui.Dummy(new Vector2(8));
|
||||
ImGui.BeginDisabled();
|
||||
}
|
||||
|
||||
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
|
||||
_uiSharedService.MediumText("Colored UID", UIColors.Get("LightlessPurple"));
|
||||
ImGui.Dummy(new Vector2(5));
|
||||
|
||||
var font = UiBuilder.MonoFont;
|
||||
var playerUID = _apiController.UID;
|
||||
var playerDisplay = _apiController.DisplayName;
|
||||
|
||||
var previewTextColor = textEnabled ? textColor : Vector4.One;
|
||||
var previewGlowColor = glowEnabled ? glowColor : Vector4.Zero;
|
||||
|
||||
var seString = SeStringUtils.BuildFormattedPlayerName(playerDisplay, previewTextColor, previewGlowColor);
|
||||
|
||||
using (ImRaii.PushFont(font))
|
||||
{
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
var textSize = ImGui.CalcTextSize(seString.TextValue);
|
||||
|
||||
float minWidth = 150f * ImGuiHelpers.GlobalScale;
|
||||
float bgWidth = Math.Max(textSize.X + 20f, minWidth);
|
||||
|
||||
float paddingY = 5f * ImGuiHelpers.GlobalScale;
|
||||
|
||||
var cursor = ImGui.GetCursorScreenPos();
|
||||
|
||||
var rectMin = cursor;
|
||||
var rectMax = rectMin + new Vector2(bgWidth, textSize.Y + (paddingY * 2f));
|
||||
|
||||
float boost = Luminance.ComputeHighlight(previewTextColor, previewGlowColor);
|
||||
|
||||
var baseBg = new Vector4(0.15f + boost, 0.15f + boost, 0.15f + boost, 1f);
|
||||
var bgColor = Luminance.BackgroundContrast(previewTextColor, previewGlowColor, baseBg, ref _currentBg);
|
||||
|
||||
var borderColor = UIColors.Get("LightlessPurple");
|
||||
|
||||
drawList.AddRectFilled(rectMin, rectMax, ImGui.GetColorU32(bgColor), 6.0f);
|
||||
drawList.AddRect(rectMin, rectMax, ImGui.GetColorU32(borderColor), 6.0f, ImDrawFlags.None, 1.5f);
|
||||
|
||||
var textPos = new Vector2(
|
||||
rectMin.X + (bgWidth - textSize.X) * 0.5f,
|
||||
rectMin.Y + paddingY
|
||||
);
|
||||
|
||||
SeStringUtils.RenderSeStringWithHitbox(seString, textPos, font);
|
||||
|
||||
ImGui.Dummy(new Vector2(5));
|
||||
}
|
||||
|
||||
const float colorPickAlign = 90f;
|
||||
|
||||
_uiSharedService.DrawNoteLine("- ", UIColors.Get("LightlessPurple"), "Text Color");
|
||||
ImGui.SameLine(colorPickAlign);
|
||||
ImGui.Checkbox("##toggleTextColor", ref textEnabled);
|
||||
ImGui.SameLine();
|
||||
ImGui.BeginDisabled(!textEnabled);
|
||||
ImGui.ColorEdit4($"##color_text", ref textColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf);
|
||||
ImGui.EndDisabled();
|
||||
|
||||
_uiSharedService.DrawNoteLine("- ", UIColors.Get("LightlessPurple"), "Glow Color");
|
||||
ImGui.SameLine(colorPickAlign);
|
||||
ImGui.Checkbox("##toggleGlowColor", ref glowEnabled);
|
||||
ImGui.SameLine();
|
||||
ImGui.BeginDisabled(!glowEnabled);
|
||||
ImGui.ColorEdit4($"##color_glow", ref glowColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf);
|
||||
ImGui.EndDisabled();
|
||||
|
||||
bool changed = !Equals(_savedVanity, new VanityState(textEnabled, glowEnabled, textColor, glowColor));
|
||||
|
||||
if (!changed)
|
||||
ImGui.BeginDisabled();
|
||||
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Changes"))
|
||||
{
|
||||
string? newText = textEnabled ? UIColors.RgbaToHex(textColor) : string.Empty;
|
||||
string? newGlow = glowEnabled ? UIColors.RgbaToHex(glowColor) : string.Empty;
|
||||
|
||||
_ = _apiController.UserUpdateVanityColors(new UserVanityColorsDto(newText, newGlow));
|
||||
|
||||
_savedVanity = new VanityState(textEnabled, glowEnabled, textColor, glowColor);
|
||||
}
|
||||
|
||||
if (!changed)
|
||||
ImGui.EndDisabled();
|
||||
|
||||
ImGui.Dummy(new Vector2(5));
|
||||
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
|
||||
|
||||
if (!hasVanity)
|
||||
ImGui.EndDisabled();
|
||||
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
|
||||
ImGui.EndTabBar();
|
||||
}
|
||||
UiSharedService.AttachToolTip("Sets your profile description text");
|
||||
ImGui.SameLine();
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear Description"))
|
||||
{
|
||||
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, ""));
|
||||
}
|
||||
UiSharedService.AttachToolTip("Clears your profile description text");
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using LightlessSync.UI.Style;
|
||||
using LightlessSync.Utils;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace LightlessSync.UI.Handlers;
|
||||
@@ -24,6 +27,9 @@ public class IdDisplayHandler
|
||||
private bool _popupShown = false;
|
||||
private DateTime? _popupTime;
|
||||
|
||||
private Vector4 _currentBg = new(0.15f, 0.15f, 0.15f, 1f);
|
||||
private float _highlightBoost;
|
||||
|
||||
public IdDisplayHandler(LightlessMediator mediator, ServerConfigurationManager serverManager, LightlessConfigService lightlessConfigService)
|
||||
{
|
||||
_mediator = mediator;
|
||||
@@ -96,31 +102,102 @@ public class IdDisplayHandler
|
||||
{
|
||||
ImGui.AlignTextToFramePadding();
|
||||
|
||||
var font = UiBuilder.MonoFont;
|
||||
var font = textIsUid ? UiBuilder.MonoFont : ImGui.GetFont();
|
||||
|
||||
var isAdmin = pair.UserData.IsAdmin;
|
||||
var isModerator = pair.UserData.IsModerator;
|
||||
Vector4? textColor = null;
|
||||
Vector4? glowColor = null;
|
||||
|
||||
Vector4? textColor = isAdmin
|
||||
? UIColors.Get("LightlessAdminText")
|
||||
: isModerator
|
||||
? UIColors.Get("LightlessModeratorText")
|
||||
: null;
|
||||
if (pair.UserData.HasVanity)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(pair.UserData.TextColorHex))
|
||||
{
|
||||
textColor = UIColors.HexToRgba(pair.UserData.TextColorHex);
|
||||
}
|
||||
|
||||
Vector4? glowColor = isAdmin
|
||||
? UIColors.Get("LightlessAdminGlow")
|
||||
: isModerator
|
||||
? UIColors.Get("LightlessModeratorGlow")
|
||||
: null;
|
||||
if (!string.IsNullOrWhiteSpace(pair.UserData.TextGlowColorHex))
|
||||
{
|
||||
glowColor = UIColors.HexToRgba(pair.UserData.TextGlowColorHex);
|
||||
}
|
||||
}
|
||||
|
||||
var seString = (textColor != null || glowColor != null)
|
||||
var useVanityColors = _lightlessConfigService.Current.useColoredUIDs && (textColor != null || glowColor != null);
|
||||
var seString = useVanityColors
|
||||
? SeStringUtils.BuildFormattedPlayerName(playerText, textColor, glowColor)
|
||||
: SeStringUtils.BuildPlain(playerText);
|
||||
|
||||
var rowStart = ImGui.GetCursorScreenPos();
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
bool useHighlight = false;
|
||||
float highlightPadX = 0f;
|
||||
float highlightPadY = 0f;
|
||||
|
||||
if (useVanityColors)
|
||||
{
|
||||
float boost = Luminance.ComputeHighlight(textColor, glowColor);
|
||||
|
||||
if (boost > 0f)
|
||||
{
|
||||
var style = ImGui.GetStyle();
|
||||
useHighlight = true;
|
||||
highlightPadX = MathF.Max(style.FramePadding.X * 0.6f, 2f * ImGuiHelpers.GlobalScale);
|
||||
highlightPadY = MathF.Max(style.FramePadding.Y * 0.55f, 1.25f * ImGuiHelpers.GlobalScale);
|
||||
drawList.ChannelsSplit(2);
|
||||
drawList.ChannelsSetCurrent(1);
|
||||
|
||||
_highlightBoost = boost;
|
||||
}
|
||||
else
|
||||
{
|
||||
_highlightBoost = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
Vector2 itemMin;
|
||||
Vector2 itemMax;
|
||||
Vector2 textSize;
|
||||
using (ImRaii.PushFont(font, textIsUid))
|
||||
{
|
||||
var pos = ImGui.GetCursorScreenPos();
|
||||
SeStringUtils.RenderSeStringWithHitbox(seString, pos, font);
|
||||
SeStringUtils.RenderSeStringWithHitbox(seString, rowStart, font);
|
||||
itemMin = ImGui.GetItemRectMin();
|
||||
itemMax = ImGui.GetItemRectMax();
|
||||
//textSize = itemMax - itemMin;
|
||||
}
|
||||
|
||||
if (useHighlight)
|
||||
{
|
||||
var style = ImGui.GetStyle();
|
||||
var frameHeight = ImGui.GetFrameHeight();
|
||||
var rowTop = rowStart.Y - style.FramePadding.Y;
|
||||
var rowBottom = rowTop + frameHeight;
|
||||
|
||||
var highlightMin = new Vector2(itemMin.X - highlightPadX, rowTop - highlightPadY);
|
||||
var highlightMax = new Vector2(itemMax.X + highlightPadX, rowBottom + highlightPadY);
|
||||
|
||||
var windowPos = ImGui.GetWindowPos();
|
||||
var contentMin = windowPos + ImGui.GetWindowContentRegionMin();
|
||||
var contentMax = windowPos + ImGui.GetWindowContentRegionMax();
|
||||
highlightMin.X = MathF.Max(highlightMin.X, contentMin.X);
|
||||
highlightMax.X = MathF.Min(highlightMax.X, contentMax.X);
|
||||
highlightMin.Y = MathF.Max(highlightMin.Y, contentMin.Y);
|
||||
highlightMax.Y = MathF.Min(highlightMax.Y, contentMax.Y);
|
||||
|
||||
var highlightColor = new Vector4(
|
||||
0.25f + _highlightBoost,
|
||||
0.25f + _highlightBoost,
|
||||
0.25f + _highlightBoost,
|
||||
1f
|
||||
);
|
||||
|
||||
highlightColor = Luminance.BackgroundContrast(textColor, glowColor, highlightColor, ref _currentBg);
|
||||
|
||||
float rounding = style.FrameRounding > 0f ? style.FrameRounding : 5f * ImGuiHelpers.GlobalScale;
|
||||
drawList.ChannelsSetCurrent(0);
|
||||
drawList.AddRectFilled(highlightMin, highlightMax, ImGui.GetColorU32(highlightColor), rounding);
|
||||
|
||||
var borderColor = style.Colors[(int)ImGuiCol.Border];
|
||||
borderColor.W *= 0.25f;
|
||||
drawList.AddRect(highlightMin, highlightMax, ImGui.GetColorU32(borderColor), rounding);
|
||||
drawList.ChannelsMerge();
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
|
||||
@@ -167,7 +167,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
else
|
||||
{
|
||||
UiSharedService.TextWrapped("To not unnecessary download files already present on your computer, Lightless Sync will have to scan your Penumbra mod directory. " +
|
||||
UiSharedService.TextWrapped("To not unnecessarily download files already present on your computer, Lightless Sync will have to scan your Penumbra mod directory. " +
|
||||
"Additionally, a local storage folder must be set where Lightless Sync will download other character files to. " +
|
||||
"Once the storage folder is set and the scan complete, this page will automatically forward to registration at a service.");
|
||||
UiSharedService.TextWrapped("Note: The initial scan, depending on the amount of mods you have, might take a while. Please wait until it is completed.");
|
||||
|
||||
@@ -63,7 +63,7 @@ internal class JoinSyncshellUI : WindowMediatorSubscriberBase
|
||||
"Joining a Syncshell will pair you implicitly with all existing users in the Syncshell." + Environment.NewLine +
|
||||
"All permissions to all users in the Syncshell will be set to the preferred Syncshell permissions on joining, excluding prior set preferred permissions.");
|
||||
ImGui.Separator();
|
||||
ImGui.TextUnformatted("Note: Syncshell ID and Password are case sensitive. MSS- is part of Syncshell IDs, unless using Vanity IDs.");
|
||||
ImGui.TextUnformatted("Note: Syncshell ID and Password are case sensitive. LLS- is part of Syncshell IDs, unless using Vanity IDs.");
|
||||
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted("Syncshell ID");
|
||||
|
||||
596
LightlessSync/UI/LightlessNotificationUI.cs
Normal file
596
LightlessSync/UI/LightlessNotificationUI.cs
Normal file
@@ -0,0 +1,596 @@
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using System.Numerics;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
|
||||
namespace LightlessSync.UI;
|
||||
|
||||
public class LightlessNotificationUI : WindowMediatorSubscriberBase
|
||||
{
|
||||
private const float NotificationMinHeight = 60f;
|
||||
private const float NotificationMaxHeight = 250f;
|
||||
private const float WindowPaddingOffset = 6f;
|
||||
private const float SlideAnimationDistance = 100f;
|
||||
private const float OutAnimationSpeedMultiplier = 0.7f;
|
||||
|
||||
private readonly List<LightlessNotification> _notifications = new();
|
||||
private readonly object _notificationLock = new();
|
||||
private readonly LightlessConfigService _configService;
|
||||
|
||||
public LightlessNotificationUI(ILogger<LightlessNotificationUI> logger, LightlessMediator mediator, PerformanceCollectorService performanceCollector, LightlessConfigService configService)
|
||||
: base(logger, mediator, "Lightless Notifications##LightlessNotifications", performanceCollector)
|
||||
{
|
||||
_configService = configService;
|
||||
Flags = ImGuiWindowFlags.NoDecoration |
|
||||
ImGuiWindowFlags.NoMove |
|
||||
ImGuiWindowFlags.NoResize |
|
||||
ImGuiWindowFlags.NoSavedSettings |
|
||||
ImGuiWindowFlags.NoFocusOnAppearing |
|
||||
ImGuiWindowFlags.NoNav |
|
||||
ImGuiWindowFlags.NoBackground |
|
||||
ImGuiWindowFlags.NoCollapse |
|
||||
ImGuiWindowFlags.AlwaysAutoResize;
|
||||
|
||||
PositionCondition = ImGuiCond.Always;
|
||||
SizeCondition = ImGuiCond.FirstUseEver;
|
||||
IsOpen = false;
|
||||
RespectCloseHotkey = false;
|
||||
DisableWindowSounds = true;
|
||||
|
||||
Mediator.Subscribe<LightlessNotificationMessage>(this, HandleNotificationMessage);
|
||||
Mediator.Subscribe<LightlessNotificationDismissMessage>(this, HandleNotificationDismissMessage);
|
||||
}
|
||||
private void HandleNotificationMessage(LightlessNotificationMessage message) =>
|
||||
AddNotification(message.Notification);
|
||||
|
||||
private void HandleNotificationDismissMessage(LightlessNotificationDismissMessage message) =>
|
||||
RemoveNotification(message.NotificationId);
|
||||
|
||||
public void AddNotification(LightlessNotification notification)
|
||||
{
|
||||
lock (_notificationLock)
|
||||
{
|
||||
var existingNotification = _notifications.FirstOrDefault(n => n.Id == notification.Id);
|
||||
if (existingNotification != null)
|
||||
{
|
||||
UpdateExistingNotification(existingNotification, notification);
|
||||
}
|
||||
else
|
||||
{
|
||||
_notifications.Add(notification);
|
||||
_logger.LogDebug("Added new notification: {Title}", notification.Title);
|
||||
}
|
||||
|
||||
if (!IsOpen) IsOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateExistingNotification(LightlessNotification existing, LightlessNotification updated)
|
||||
{
|
||||
existing.Message = updated.Message;
|
||||
existing.Progress = updated.Progress;
|
||||
existing.ShowProgress = updated.ShowProgress;
|
||||
existing.Title = updated.Title;
|
||||
_logger.LogDebug("Updated existing notification: {Title}", updated.Title);
|
||||
}
|
||||
|
||||
public void RemoveNotification(string id)
|
||||
{
|
||||
lock (_notificationLock)
|
||||
{
|
||||
var notification = _notifications.FirstOrDefault(n => n.Id == id);
|
||||
if (notification != null)
|
||||
{
|
||||
StartOutAnimation(notification);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void StartOutAnimation(LightlessNotification notification)
|
||||
{
|
||||
notification.IsAnimatingOut = true;
|
||||
notification.IsAnimatingIn = false;
|
||||
}
|
||||
|
||||
protected override void DrawInternal()
|
||||
{
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero);
|
||||
|
||||
lock (_notificationLock)
|
||||
{
|
||||
UpdateNotifications();
|
||||
|
||||
if (_notifications.Count == 0)
|
||||
{
|
||||
ImGui.PopStyleVar();
|
||||
IsOpen = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var viewport = ImGui.GetMainViewport();
|
||||
Position = CalculateWindowPosition(viewport);
|
||||
DrawAllNotifications();
|
||||
}
|
||||
|
||||
ImGui.PopStyleVar();
|
||||
}
|
||||
|
||||
private Vector2 CalculateWindowPosition(ImGuiViewportPtr viewport)
|
||||
{
|
||||
var x = viewport.WorkPos.X + viewport.WorkSize.X -
|
||||
_configService.Current.NotificationWidth -
|
||||
_configService.Current.NotificationOffsetX -
|
||||
WindowPaddingOffset;
|
||||
var y = viewport.WorkPos.Y + _configService.Current.NotificationOffsetY;
|
||||
return new Vector2(x, y);
|
||||
}
|
||||
|
||||
private void DrawAllNotifications()
|
||||
{
|
||||
for (int i = 0; i < _notifications.Count; i++)
|
||||
{
|
||||
DrawNotification(_notifications[i], i);
|
||||
|
||||
if (i < _notifications.Count - 1)
|
||||
{
|
||||
ImGui.Dummy(new Vector2(0, _configService.Current.NotificationSpacing));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateNotifications()
|
||||
{
|
||||
var deltaTime = ImGui.GetIO().DeltaTime;
|
||||
EnforceMaxNotificationLimit();
|
||||
UpdateAnimationsAndRemoveExpired(deltaTime);
|
||||
}
|
||||
|
||||
private void EnforceMaxNotificationLimit()
|
||||
{
|
||||
var maxNotifications = _configService.Current.MaxSimultaneousNotifications;
|
||||
while (_notifications.Count(n => !n.IsAnimatingOut) > maxNotifications)
|
||||
{
|
||||
var oldestNotification = _notifications
|
||||
.Where(n => !n.IsAnimatingOut)
|
||||
.OrderBy(n => n.CreatedAt)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (oldestNotification != null)
|
||||
{
|
||||
StartOutAnimation(oldestNotification);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAnimationsAndRemoveExpired(float deltaTime)
|
||||
{
|
||||
for (int i = _notifications.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var notification = _notifications[i];
|
||||
UpdateNotificationAnimation(notification, deltaTime);
|
||||
|
||||
if (ShouldRemoveNotification(notification))
|
||||
{
|
||||
_notifications.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateNotificationAnimation(LightlessNotification notification, float deltaTime)
|
||||
{
|
||||
if (notification.IsAnimatingIn && notification.AnimationProgress < 1f)
|
||||
{
|
||||
notification.AnimationProgress = Math.Min(1f,
|
||||
notification.AnimationProgress + deltaTime * _configService.Current.NotificationAnimationSpeed);
|
||||
}
|
||||
else if (notification.IsAnimatingOut && notification.AnimationProgress > 0f)
|
||||
{
|
||||
notification.AnimationProgress = Math.Max(0f,
|
||||
notification.AnimationProgress - deltaTime * _configService.Current.NotificationAnimationSpeed * OutAnimationSpeedMultiplier);
|
||||
}
|
||||
else if (!notification.IsAnimatingOut && !notification.IsDismissed)
|
||||
{
|
||||
notification.IsAnimatingIn = false;
|
||||
|
||||
if (notification.IsExpired)
|
||||
{
|
||||
StartOutAnimation(notification);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldRemoveNotification(LightlessNotification notification) =>
|
||||
notification.IsAnimatingOut && notification.AnimationProgress <= 0.01f;
|
||||
|
||||
private void DrawNotification(LightlessNotification notification, int index)
|
||||
{
|
||||
var alpha = notification.AnimationProgress;
|
||||
if (alpha <= 0f) return;
|
||||
|
||||
var slideOffset = (1f - alpha) * SlideAnimationDistance;
|
||||
var originalCursorPos = ImGui.GetCursorPos();
|
||||
ImGui.SetCursorPosX(originalCursorPos.X + slideOffset);
|
||||
|
||||
var notificationHeight = CalculateNotificationHeight(notification);
|
||||
var notificationWidth = _configService.Current.NotificationWidth - slideOffset;
|
||||
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero);
|
||||
|
||||
using var child = ImRaii.Child($"notification_{notification.Id}",
|
||||
new Vector2(notificationWidth, notificationHeight),
|
||||
false, ImGuiWindowFlags.NoScrollbar);
|
||||
|
||||
if (child.Success)
|
||||
{
|
||||
DrawNotificationContent(notification, alpha);
|
||||
}
|
||||
|
||||
ImGui.PopStyleVar();
|
||||
}
|
||||
|
||||
private void DrawNotificationContent(LightlessNotification notification, float alpha)
|
||||
{
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
var windowPos = ImGui.GetWindowPos();
|
||||
var windowSize = ImGui.GetWindowSize();
|
||||
|
||||
var bgColor = CalculateBackgroundColor(alpha, ImGui.IsWindowHovered());
|
||||
var accentColor = GetNotificationAccentColor(notification.Type);
|
||||
accentColor.W *= alpha;
|
||||
|
||||
DrawShadow(drawList, windowPos, windowSize, alpha);
|
||||
HandleClickToDismiss(notification);
|
||||
DrawBackground(drawList, windowPos, windowSize, bgColor);
|
||||
DrawAccentBar(drawList, windowPos, windowSize, accentColor);
|
||||
DrawDurationProgressBar(notification, alpha, windowPos, windowSize, drawList);
|
||||
DrawNotificationText(notification, alpha);
|
||||
}
|
||||
|
||||
private Vector4 CalculateBackgroundColor(float alpha, bool isHovered)
|
||||
{
|
||||
var baseOpacity = _configService.Current.NotificationOpacity;
|
||||
var finalOpacity = baseOpacity * alpha;
|
||||
var bgColor = new Vector4(30f/255f, 30f/255f, 30f/255f, finalOpacity);
|
||||
|
||||
if (isHovered)
|
||||
{
|
||||
bgColor *= 1.1f;
|
||||
bgColor.W = Math.Min(bgColor.W, 0.98f);
|
||||
}
|
||||
|
||||
return bgColor;
|
||||
}
|
||||
|
||||
private void DrawShadow(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float alpha)
|
||||
{
|
||||
var shadowOffset = new Vector2(1f, 1f);
|
||||
var shadowColor = new Vector4(0f, 0f, 0f, 0.4f * alpha);
|
||||
drawList.AddRectFilled(
|
||||
windowPos + shadowOffset,
|
||||
windowPos + windowSize + shadowOffset,
|
||||
ImGui.ColorConvertFloat4ToU32(shadowColor),
|
||||
3f
|
||||
);
|
||||
}
|
||||
|
||||
private void HandleClickToDismiss(LightlessNotification notification)
|
||||
{
|
||||
if (ImGui.IsWindowHovered() &&
|
||||
_configService.Current.DismissNotificationOnClick &&
|
||||
!notification.Actions.Any() &&
|
||||
ImGui.IsMouseClicked(ImGuiMouseButton.Left))
|
||||
{
|
||||
notification.IsDismissed = true;
|
||||
StartOutAnimation(notification);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, Vector4 bgColor)
|
||||
{
|
||||
drawList.AddRectFilled(
|
||||
windowPos,
|
||||
windowPos + windowSize,
|
||||
ImGui.ColorConvertFloat4ToU32(bgColor),
|
||||
3f
|
||||
);
|
||||
}
|
||||
|
||||
private void DrawAccentBar(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, Vector4 accentColor)
|
||||
{
|
||||
var accentWidth = _configService.Current.NotificationAccentBarWidth;
|
||||
if (accentWidth > 0f)
|
||||
{
|
||||
drawList.AddRectFilled(
|
||||
windowPos,
|
||||
windowPos + new Vector2(accentWidth, windowSize.Y),
|
||||
ImGui.ColorConvertFloat4ToU32(accentColor),
|
||||
3f
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawDurationProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, ImDrawListPtr drawList)
|
||||
{
|
||||
var progress = CalculateProgress(notification);
|
||||
var progressBarColor = UIColors.Get("LightlessBlue");
|
||||
var progressHeight = 2f;
|
||||
var progressY = windowPos.Y + windowSize.Y - progressHeight;
|
||||
var progressWidth = windowSize.X * progress;
|
||||
|
||||
DrawProgressBackground(drawList, windowPos, windowSize, progressY, progressHeight, progressBarColor, alpha);
|
||||
|
||||
if (progress > 0)
|
||||
{
|
||||
DrawProgressForeground(drawList, windowPos, progressY, progressHeight, progressWidth, progressBarColor, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
private float CalculateProgress(LightlessNotification notification)
|
||||
{
|
||||
if (notification.Type == NotificationType.Download && notification.ShowProgress)
|
||||
{
|
||||
return Math.Clamp(notification.Progress, 0f, 1f);
|
||||
}
|
||||
|
||||
var elapsed = DateTime.UtcNow - notification.CreatedAt;
|
||||
return Math.Min(1.0f, (float)(elapsed.TotalSeconds / notification.Duration.TotalSeconds));
|
||||
}
|
||||
|
||||
private void DrawProgressBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float progressY, float progressHeight, Vector4 progressBarColor, float alpha)
|
||||
{
|
||||
var bgProgressColor = new Vector4(progressBarColor.X * 0.3f, progressBarColor.Y * 0.3f, progressBarColor.Z * 0.3f, 0.5f * alpha);
|
||||
drawList.AddRectFilled(
|
||||
new Vector2(windowPos.X, progressY),
|
||||
new Vector2(windowPos.X + windowSize.X, progressY + progressHeight),
|
||||
ImGui.ColorConvertFloat4ToU32(bgProgressColor),
|
||||
0f
|
||||
);
|
||||
}
|
||||
|
||||
private void DrawProgressForeground(ImDrawListPtr drawList, Vector2 windowPos, float progressY, float progressHeight, float progressWidth, Vector4 progressBarColor, float alpha)
|
||||
{
|
||||
var progressColor = progressBarColor;
|
||||
progressColor.W *= alpha;
|
||||
drawList.AddRectFilled(
|
||||
new Vector2(windowPos.X, progressY),
|
||||
new Vector2(windowPos.X + progressWidth, progressY + progressHeight),
|
||||
ImGui.ColorConvertFloat4ToU32(progressColor),
|
||||
0f
|
||||
);
|
||||
}
|
||||
|
||||
private void DrawNotificationText(LightlessNotification notification, float alpha)
|
||||
{
|
||||
var padding = new Vector2(10f, 6f);
|
||||
var contentPos = new Vector2(padding.X, padding.Y);
|
||||
var windowSize = ImGui.GetWindowSize();
|
||||
var contentSize = new Vector2(windowSize.X - padding.X, windowSize.Y - padding.Y * 2);
|
||||
|
||||
ImGui.SetCursorPos(contentPos);
|
||||
|
||||
var titleHeight = DrawTitle(notification, contentSize.X, alpha);
|
||||
DrawMessage(notification, contentPos, contentSize.X, titleHeight, alpha);
|
||||
|
||||
if (notification.Actions.Count > 0)
|
||||
{
|
||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + ImGui.GetStyle().ItemSpacing.Y);
|
||||
ImGui.SetCursorPosX(contentPos.X);
|
||||
DrawNotificationActions(notification, contentSize.X, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
private float DrawTitle(LightlessNotification notification, float contentWidth, float alpha)
|
||||
{
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(1f, 1f, 1f, alpha)))
|
||||
{
|
||||
ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + contentWidth);
|
||||
var titleStartY = ImGui.GetCursorPosY();
|
||||
|
||||
var titleText = _configService.Current.ShowNotificationTimestamp
|
||||
? $"[{notification.CreatedAt.ToLocalTime():HH:mm:ss}] {notification.Title}"
|
||||
: notification.Title;
|
||||
|
||||
ImGui.TextWrapped(titleText);
|
||||
var titleHeight = ImGui.GetCursorPosY() - titleStartY;
|
||||
ImGui.PopTextWrapPos();
|
||||
return titleHeight;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawMessage(LightlessNotification notification, Vector2 contentPos, float contentWidth, float titleHeight, float alpha)
|
||||
{
|
||||
if (string.IsNullOrEmpty(notification.Message)) return;
|
||||
|
||||
ImGui.SetCursorPos(contentPos + new Vector2(0f, titleHeight + 4f));
|
||||
ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + contentWidth);
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(0.9f, 0.9f, 0.9f, alpha)))
|
||||
{
|
||||
ImGui.TextWrapped(notification.Message);
|
||||
}
|
||||
ImGui.PopTextWrapPos();
|
||||
}
|
||||
|
||||
private void DrawNotificationActions(LightlessNotification notification, float availableWidth, float alpha)
|
||||
{
|
||||
var buttonSpacing = 8f;
|
||||
var rightPadding = 10f;
|
||||
var usableWidth = availableWidth - rightPadding;
|
||||
var totalSpacing = (notification.Actions.Count - 1) * buttonSpacing;
|
||||
var buttonWidth = (usableWidth - totalSpacing) / notification.Actions.Count;
|
||||
|
||||
_logger.LogDebug("Drawing {ActionCount} notification actions, buttonWidth: {ButtonWidth}, availableWidth: {AvailableWidth}",
|
||||
notification.Actions.Count, buttonWidth, availableWidth);
|
||||
|
||||
var startCursorPos = ImGui.GetCursorPos();
|
||||
|
||||
for (int i = 0; i < notification.Actions.Count; i++)
|
||||
{
|
||||
var action = notification.Actions[i];
|
||||
|
||||
if (i > 0)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
var currentX = startCursorPos.X + i * (buttonWidth + buttonSpacing);
|
||||
ImGui.SetCursorPosX(currentX);
|
||||
}
|
||||
DrawActionButton(action, notification, alpha, buttonWidth);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawActionButton(LightlessNotificationAction action, LightlessNotification notification, float alpha, float buttonWidth)
|
||||
{
|
||||
_logger.LogDebug("Drawing action button: {ActionId} - {ActionLabel}, width: {ButtonWidth}", action.Id, action.Label, buttonWidth);
|
||||
|
||||
var buttonColor = action.Color;
|
||||
buttonColor.W *= alpha;
|
||||
|
||||
var hoveredColor = buttonColor * 1.1f;
|
||||
hoveredColor.W = buttonColor.W;
|
||||
|
||||
var activeColor = buttonColor * 0.9f;
|
||||
activeColor.W = buttonColor.W;
|
||||
|
||||
using (ImRaii.PushColor(ImGuiCol.Button, buttonColor))
|
||||
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, hoveredColor))
|
||||
using (ImRaii.PushColor(ImGuiCol.ButtonActive, activeColor))
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(1f, 1f, 1f, alpha)))
|
||||
{
|
||||
var buttonPressed = false;
|
||||
|
||||
if (action.Icon != FontAwesomeIcon.None)
|
||||
{
|
||||
buttonPressed = DrawIconTextButton(action.Icon, action.Label, buttonWidth, alpha);
|
||||
}
|
||||
else
|
||||
{
|
||||
buttonPressed = ImGui.Button(action.Label, new Vector2(buttonWidth, 0));
|
||||
}
|
||||
|
||||
_logger.LogDebug("Button {ActionId} pressed: {ButtonPressed}", action.Id, buttonPressed);
|
||||
|
||||
if (buttonPressed)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Executing action: {ActionId}", action.Id);
|
||||
action.OnClick(notification);
|
||||
_logger.LogDebug("Action executed successfully: {ActionId}", action.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error executing notification action: {ActionId}", action.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool DrawIconTextButton(FontAwesomeIcon icon, string text, float width, float alpha)
|
||||
{
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
var cursorPos = ImGui.GetCursorScreenPos();
|
||||
var frameHeight = ImGui.GetFrameHeight();
|
||||
|
||||
Vector2 iconSize;
|
||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||
{
|
||||
iconSize = ImGui.CalcTextSize(icon.ToIconString());
|
||||
}
|
||||
|
||||
var textSize = ImGui.CalcTextSize(text);
|
||||
var spacing = 3f * ImGuiHelpers.GlobalScale;
|
||||
var totalTextWidth = iconSize.X + spacing + textSize.X;
|
||||
|
||||
var buttonPressed = ImGui.InvisibleButton($"btn_{icon}_{text}", new Vector2(width, frameHeight));
|
||||
|
||||
var buttonMin = ImGui.GetItemRectMin();
|
||||
var buttonMax = ImGui.GetItemRectMax();
|
||||
var buttonSize = buttonMax - buttonMin;
|
||||
|
||||
var buttonColor = ImGui.GetColorU32(ImGuiCol.Button);
|
||||
if (ImGui.IsItemHovered())
|
||||
buttonColor = ImGui.GetColorU32(ImGuiCol.ButtonHovered);
|
||||
if (ImGui.IsItemActive())
|
||||
buttonColor = ImGui.GetColorU32(ImGuiCol.ButtonActive);
|
||||
|
||||
drawList.AddRectFilled(buttonMin, buttonMax, buttonColor, 3f);
|
||||
|
||||
var iconPos = buttonMin + new Vector2((buttonSize.X - totalTextWidth) / 2f, (buttonSize.Y - iconSize.Y) / 2f);
|
||||
var textPos = iconPos + new Vector2(iconSize.X + spacing, (iconSize.Y - textSize.Y) / 2f);
|
||||
|
||||
var textColor = ImGui.GetColorU32(ImGuiCol.Text);
|
||||
|
||||
// Draw icon
|
||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||
{
|
||||
drawList.AddText(iconPos, textColor, icon.ToIconString());
|
||||
}
|
||||
|
||||
// Draw text
|
||||
drawList.AddText(textPos, textColor, text);
|
||||
|
||||
return buttonPressed;
|
||||
}
|
||||
|
||||
private float CalculateNotificationHeight(LightlessNotification notification)
|
||||
{
|
||||
var contentWidth = _configService.Current.NotificationWidth - 35f;
|
||||
var height = 12f;
|
||||
|
||||
height += CalculateTitleHeight(notification, contentWidth);
|
||||
height += CalculateMessageHeight(notification, contentWidth);
|
||||
|
||||
if (notification.ShowProgress)
|
||||
{
|
||||
height += 12f;
|
||||
}
|
||||
|
||||
if (notification.Actions.Count > 0)
|
||||
{
|
||||
height += ImGui.GetStyle().ItemSpacing.Y;
|
||||
height += ImGui.GetFrameHeight();
|
||||
height += 12f;
|
||||
}
|
||||
|
||||
return Math.Clamp(height, NotificationMinHeight, NotificationMaxHeight);
|
||||
}
|
||||
|
||||
private float CalculateTitleHeight(LightlessNotification notification, float contentWidth)
|
||||
{
|
||||
var titleText = _configService.Current.ShowNotificationTimestamp
|
||||
? $"[{notification.CreatedAt.ToLocalTime():HH:mm:ss}] {notification.Title}"
|
||||
: notification.Title;
|
||||
|
||||
return ImGui.CalcTextSize(titleText, true, contentWidth).Y;
|
||||
}
|
||||
|
||||
private float CalculateMessageHeight(LightlessNotification notification, float contentWidth)
|
||||
{
|
||||
if (string.IsNullOrEmpty(notification.Message)) return 0f;
|
||||
|
||||
var messageHeight = ImGui.CalcTextSize(notification.Message, true, contentWidth).Y;
|
||||
return 4f + messageHeight;
|
||||
}
|
||||
|
||||
private Vector4 GetNotificationAccentColor(NotificationType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
NotificationType.Info => UIColors.Get("LightlessPurple"),
|
||||
NotificationType.Warning => UIColors.Get("LightlessYellow"),
|
||||
NotificationType.Error => UIColors.Get("DimRed"),
|
||||
NotificationType.PairRequest => UIColors.Get("LightlessBlue"),
|
||||
NotificationType.Download => UIColors.Get("LightlessGreen"),
|
||||
_ => UIColors.Get("LightlessPurple")
|
||||
};
|
||||
}
|
||||
}
|
||||
32
LightlessSync/UI/Models/LightlessNotification.cs
Normal file
32
LightlessSync/UI/Models/LightlessNotification.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Dalamud.Interface;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using System.Numerics;
|
||||
namespace LightlessSync.UI.Models;
|
||||
public class LightlessNotification
|
||||
{
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public NotificationType Type { get; set; } = NotificationType.Info;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public TimeSpan Duration { get; set; } = TimeSpan.FromSeconds(5);
|
||||
public bool IsExpired => DateTime.UtcNow - CreatedAt > Duration;
|
||||
public bool IsDismissed { get; set; } = false;
|
||||
public List<LightlessNotificationAction> Actions { get; set; } = new();
|
||||
public bool ShowProgress { get; set; } = false;
|
||||
public float Progress { get; set; } = 0f;
|
||||
public float AnimationProgress { get; set; } = 0f;
|
||||
public bool IsAnimatingIn { get; set; } = true;
|
||||
public bool IsAnimatingOut { get; set; } = false;
|
||||
public uint? SoundEffectId { get; set; } = null;
|
||||
}
|
||||
public class LightlessNotificationAction
|
||||
{
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||
public string Label { get; set; } = string.Empty;
|
||||
public FontAwesomeIcon Icon { get; set; } = FontAwesomeIcon.None;
|
||||
public Vector4 Color { get; set; } = Vector4.One;
|
||||
public Action<LightlessNotification> OnClick { get; set; } = _ => { };
|
||||
public bool IsPrimary { get; set; } = false;
|
||||
public bool IsDestructive { get; set; } = false;
|
||||
}
|
||||
72
LightlessSync/UI/Models/NotificationSounds.cs
Normal file
72
LightlessSync/UI/Models/NotificationSounds.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
|
||||
namespace LightlessSync.UI.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Common FFXIV <se.#> sound effect IDs for notifications.
|
||||
/// These correspond to the same sound IDs used in macros (1–16).
|
||||
/// </summary>
|
||||
public static class NotificationSounds
|
||||
{
|
||||
// ─────────────────────────────────────────────
|
||||
// Base <se.#> IDs (1–16)
|
||||
// https://ffxiv.consolegameswiki.com/wiki/Macros#Sound_Effects
|
||||
// ─────────────────────────────────────────────
|
||||
public const uint Se1 = 1; // Soft chime
|
||||
public const uint Se2 = 2; // Higher chime
|
||||
public const uint Se3 = 3; // Bell tone
|
||||
public const uint Se4 = 4; // Harp tone
|
||||
public const uint Se5 = 5; // Mechanical click
|
||||
public const uint Se6 = 6; // Drum / percussion
|
||||
public const uint Se7 = 7; // Metallic chime
|
||||
public const uint Se8 = 8; // Wooden tone
|
||||
public const uint Se9 = 9; // Wind / flute tone
|
||||
public const uint Se10 = 11; // Magical sparkle (ID 10 is skipped in game)
|
||||
public const uint Se11 = 12; // Metallic ring
|
||||
public const uint Se12 = 13; // Deep thud
|
||||
public const uint Se13 = 14; // "Tell received" ping
|
||||
public const uint Se14 = 15; // Success fanfare
|
||||
public const uint Se15 = 16; // System warning
|
||||
// Note: Se16 doesn't exist - Se15 is the last available sound
|
||||
|
||||
/// <summary>
|
||||
/// General notification sound (<se.2>)
|
||||
/// </summary>
|
||||
public const uint Info = Se2;
|
||||
|
||||
/// <summary>
|
||||
/// Warning/alert sound (<se.15>)
|
||||
/// </summary>
|
||||
public const uint Warning = Se15;
|
||||
|
||||
/// <summary>
|
||||
/// Error sound (<se.15> - System warning, used for errors)
|
||||
/// </summary>
|
||||
public const uint Error = Se15;
|
||||
|
||||
/// <summary>
|
||||
/// Success sound (<se.14>)
|
||||
/// </summary>
|
||||
public const uint Success = Se14;
|
||||
|
||||
/// <summary>
|
||||
/// Pair request sound (<se.13>, same as tell notification)
|
||||
/// </summary>
|
||||
public const uint PairRequest = Se13;
|
||||
|
||||
/// <summary>
|
||||
/// Download complete sound (<se.10>, a clean sparkle tone)
|
||||
/// </summary>
|
||||
public const uint DownloadComplete = Se10;
|
||||
|
||||
/// <summary>
|
||||
/// Get default sound for notification type
|
||||
/// </summary>
|
||||
public static uint GetDefaultSound(NotificationType type) => type switch
|
||||
{
|
||||
NotificationType.Info => Info,
|
||||
NotificationType.Warning => Warning,
|
||||
NotificationType.Error => Error,
|
||||
_ => Info
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
64
LightlessSync/UI/Style/Luminance.cs
Normal file
64
LightlessSync/UI/Style/Luminance.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace LightlessSync.UI.Style
|
||||
{
|
||||
internal static class Luminance
|
||||
{
|
||||
public static float BrightnessThreshold { get; set; } = 0.4f;
|
||||
public static float HighlightBoostMax { get; set; } = 0.1f;
|
||||
public static float SmoothFactor { get; set; } = 0.15f;
|
||||
|
||||
private static float Brightness(Vector4 color)
|
||||
=> Math.Max(color.X, Math.Max(color.Y, color.Z));
|
||||
|
||||
public static float ComputeHighlight(Vector4? textColor, Vector4? glowColor)
|
||||
{
|
||||
float brightnessText = textColor.HasValue ? Brightness(textColor.Value) : 1f;
|
||||
float brightnessGlow = glowColor.HasValue ? Brightness(glowColor.Value) : 1f;
|
||||
|
||||
if (brightnessText >= BrightnessThreshold || brightnessGlow >= BrightnessThreshold)
|
||||
return 0f;
|
||||
|
||||
float deficit = Math.Min(BrightnessThreshold - brightnessText,
|
||||
BrightnessThreshold - brightnessGlow);
|
||||
|
||||
float factor = Math.Clamp(deficit / BrightnessThreshold, 0f, 1f);
|
||||
factor = MathF.Pow(factor, 2.0f);
|
||||
|
||||
return factor * HighlightBoostMax;
|
||||
}
|
||||
|
||||
public static Vector4 BackgroundContrast(Vector4? textColor, Vector4? glowColor, Vector4 backgroundColor, ref Vector4 currentBg)
|
||||
{
|
||||
if (!textColor.HasValue && !glowColor.HasValue)
|
||||
return backgroundColor;
|
||||
|
||||
float brightnessText = textColor.HasValue ? Brightness(textColor.Value) : 0f;
|
||||
float brightnessGlow = glowColor.HasValue ? Brightness(glowColor.Value) : 0f;
|
||||
|
||||
float fgBrightness = Math.Max(brightnessText, brightnessGlow);
|
||||
float bgBrightness = Brightness(backgroundColor);
|
||||
float diff = Math.Abs(bgBrightness - fgBrightness);
|
||||
|
||||
bool shouldBeDark = fgBrightness > 0.5f;
|
||||
Vector4 targetBg;
|
||||
|
||||
if (diff >= BrightnessThreshold)
|
||||
{
|
||||
targetBg = backgroundColor;
|
||||
}
|
||||
else
|
||||
{
|
||||
targetBg = shouldBeDark
|
||||
? new Vector4(0.05f, 0.05f, 0.05f, backgroundColor.W)
|
||||
: new Vector4(0.95f, 0.95f, 0.95f, backgroundColor.W);
|
||||
}
|
||||
|
||||
float t = Math.Clamp(SmoothFactor, 0f, 1f);
|
||||
currentBg = t <= 0f ? targetBg : Vector4.Lerp(currentBg, targetBg, t);
|
||||
|
||||
return currentBg;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,169 +1,231 @@
|
||||
// inspiration: brio because it's style is fucking amazing
|
||||
// inspiration: brio because it's style is fucking amazing
|
||||
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.LightlessConfiguration.Configurations;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace LightlessSync.UI.Style
|
||||
namespace LightlessSync.UI.Style;
|
||||
|
||||
internal static class MainStyle
|
||||
{
|
||||
internal static class MainStyle
|
||||
public readonly record struct StyleColorOption(string Key, string Label, Func<Vector4> DefaultValue, ImGuiCol Target, string? Description = null, string? UiColorKey = null);
|
||||
public readonly record struct StyleFloatOption(string Key, string Label, float DefaultValue, ImGuiStyleVar Target, float? Min = null, float? Max = null, float Speed = 0.25f, string? Description = null);
|
||||
public readonly record struct StyleVector2Option(string Key, string Label, Func<Vector2> DefaultValue, ImGuiStyleVar Target, Vector2? Min = null, Vector2? Max = null, float Speed = 0.25f, string? Description = null);
|
||||
|
||||
private static LightlessConfigService? _config;
|
||||
private static UiThemeConfigService? _themeConfig;
|
||||
public static void Init(LightlessConfigService config, UiThemeConfigService themeConfig)
|
||||
{
|
||||
private static LightlessConfigService? _config;
|
||||
public static void Init(LightlessConfigService config) => _config = config;
|
||||
public static bool ShouldUseTheme => _config?.Current.UseLightlessRedesign ?? false;
|
||||
|
||||
private static bool _hasPushed;
|
||||
private static int _pushedColorCount;
|
||||
private static int _pushedStyleVarCount;
|
||||
|
||||
public static void PushStyle()
|
||||
{
|
||||
if (_hasPushed)
|
||||
PopStyle();
|
||||
|
||||
if (!ShouldUseTheme)
|
||||
{
|
||||
_hasPushed = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_hasPushed = true;
|
||||
_pushedColorCount = 0;
|
||||
_pushedStyleVarCount = 0;
|
||||
|
||||
Push(ImGuiCol.Text, new Vector4(255, 255, 255, 255));
|
||||
Push(ImGuiCol.TextDisabled, new Vector4(128, 128, 128, 255));
|
||||
|
||||
Push(ImGuiCol.WindowBg, new Vector4(23, 23, 23, 248));
|
||||
Push(ImGuiCol.ChildBg, new Vector4(23, 23, 23, 66));
|
||||
Push(ImGuiCol.PopupBg, new Vector4(23, 23, 23, 248));
|
||||
|
||||
Push(ImGuiCol.Border, new Vector4(65, 65, 65, 255));
|
||||
Push(ImGuiCol.BorderShadow, new Vector4(0, 0, 0, 150));
|
||||
|
||||
Push(ImGuiCol.FrameBg, new Vector4(40, 40, 40, 255));
|
||||
Push(ImGuiCol.FrameBgHovered, new Vector4(50, 50, 50, 255));
|
||||
Push(ImGuiCol.FrameBgActive, new Vector4(30, 30, 30, 255));
|
||||
|
||||
Push(ImGuiCol.TitleBg, new Vector4(24, 24, 24, 232));
|
||||
Push(ImGuiCol.TitleBgActive, new Vector4(30, 30, 30, 255));
|
||||
Push(ImGuiCol.TitleBgCollapsed, new Vector4(27, 27, 27, 255));
|
||||
|
||||
Push(ImGuiCol.MenuBarBg, new Vector4(36, 36, 36, 255));
|
||||
Push(ImGuiCol.ScrollbarBg, new Vector4(0, 0, 0, 0));
|
||||
Push(ImGuiCol.ScrollbarGrab, new Vector4(62, 62, 62, 255));
|
||||
Push(ImGuiCol.ScrollbarGrabHovered, new Vector4(70, 70, 70, 255));
|
||||
Push(ImGuiCol.ScrollbarGrabActive, new Vector4(70, 70, 70, 255));
|
||||
|
||||
Push(ImGuiCol.CheckMark, UIColors.Get("LightlessPurple"));
|
||||
|
||||
Push(ImGuiCol.SliderGrab, new Vector4(101, 101, 101, 255));
|
||||
Push(ImGuiCol.SliderGrabActive, new Vector4(123, 123, 123, 255));
|
||||
|
||||
Push(ImGuiCol.Button, UIColors.Get("ButtonDefault"));
|
||||
Push(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurple"));
|
||||
Push(ImGuiCol.ButtonActive, UIColors.Get("LightlessPurpleActive"));
|
||||
|
||||
Push(ImGuiCol.Header, new Vector4(0, 0, 0, 60));
|
||||
Push(ImGuiCol.HeaderHovered, new Vector4(0, 0, 0, 90));
|
||||
Push(ImGuiCol.HeaderActive, new Vector4(0, 0, 0, 120));
|
||||
|
||||
Push(ImGuiCol.Separator, new Vector4(75, 75, 75, 121));
|
||||
Push(ImGuiCol.SeparatorHovered, UIColors.Get("LightlessPurple"));
|
||||
Push(ImGuiCol.SeparatorActive, UIColors.Get("LightlessPurpleActive"));
|
||||
|
||||
Push(ImGuiCol.ResizeGrip, new Vector4(0, 0, 0, 0));
|
||||
Push(ImGuiCol.ResizeGripHovered, new Vector4(0, 0, 0, 0));
|
||||
Push(ImGuiCol.ResizeGripActive, UIColors.Get("LightlessPurpleActive"));
|
||||
|
||||
Push(ImGuiCol.Tab, new Vector4(40, 40, 40, 255));
|
||||
Push(ImGuiCol.TabHovered, UIColors.Get("LightlessPurple"));
|
||||
Push(ImGuiCol.TabActive, UIColors.Get("LightlessPurpleActive"));
|
||||
Push(ImGuiCol.TabUnfocused, new Vector4(40, 40, 40, 255));
|
||||
Push(ImGuiCol.TabUnfocusedActive, UIColors.Get("LightlessPurpleActive"));
|
||||
|
||||
Push(ImGuiCol.DockingPreview, UIColors.Get("LightlessPurpleActive"));
|
||||
Push(ImGuiCol.DockingEmptyBg, new Vector4(50, 50, 50, 255));
|
||||
|
||||
Push(ImGuiCol.PlotLines, new Vector4(150, 150, 150, 255));
|
||||
|
||||
Push(ImGuiCol.TableHeaderBg, new Vector4(48, 48, 48, 255));
|
||||
Push(ImGuiCol.TableBorderStrong, new Vector4(79, 79, 89, 255));
|
||||
Push(ImGuiCol.TableBorderLight, new Vector4(59, 59, 64, 255));
|
||||
Push(ImGuiCol.TableRowBg, new Vector4(0, 0, 0, 0));
|
||||
Push(ImGuiCol.TableRowBgAlt, new Vector4(255, 255, 255, 15));
|
||||
|
||||
Push(ImGuiCol.TextSelectedBg, new Vector4(98, 75, 224, 255));
|
||||
Push(ImGuiCol.DragDropTarget, new Vector4(98, 75, 224, 255));
|
||||
|
||||
Push(ImGuiCol.NavHighlight, new Vector4(98, 75, 224, 179));
|
||||
Push(ImGuiCol.NavWindowingDimBg, new Vector4(204, 204, 204, 51));
|
||||
Push(ImGuiCol.NavWindowingHighlight, new Vector4(204, 204, 204, 89));
|
||||
|
||||
PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(6, 6));
|
||||
PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(4, 3));
|
||||
PushStyleVar(ImGuiStyleVar.CellPadding, new Vector2(4, 4));
|
||||
PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(4, 4));
|
||||
PushStyleVar(ImGuiStyleVar.ItemInnerSpacing, new Vector2(4, 4));
|
||||
|
||||
PushStyleVar(ImGuiStyleVar.IndentSpacing, 21.0f);
|
||||
PushStyleVar(ImGuiStyleVar.ScrollbarSize, 10.0f);
|
||||
PushStyleVar(ImGuiStyleVar.GrabMinSize, 20.0f);
|
||||
|
||||
PushStyleVar(ImGuiStyleVar.WindowBorderSize, 1.5f);
|
||||
PushStyleVar(ImGuiStyleVar.ChildBorderSize, 1.5f);
|
||||
PushStyleVar(ImGuiStyleVar.PopupBorderSize, 1.5f);
|
||||
PushStyleVar(ImGuiStyleVar.FrameBorderSize, 0f);
|
||||
|
||||
PushStyleVar(ImGuiStyleVar.WindowRounding, 7f);
|
||||
PushStyleVar(ImGuiStyleVar.ChildRounding, 4f);
|
||||
PushStyleVar(ImGuiStyleVar.FrameRounding, 4f);
|
||||
PushStyleVar(ImGuiStyleVar.PopupRounding, 4f);
|
||||
PushStyleVar(ImGuiStyleVar.ScrollbarRounding, 4f);
|
||||
PushStyleVar(ImGuiStyleVar.GrabRounding, 4f);
|
||||
PushStyleVar(ImGuiStyleVar.TabRounding, 4f);
|
||||
}
|
||||
|
||||
public static void PopStyle()
|
||||
{
|
||||
if (!_hasPushed)
|
||||
return;
|
||||
|
||||
if (_pushedStyleVarCount > 0)
|
||||
ImGui.PopStyleVar(_pushedStyleVarCount);
|
||||
if (_pushedColorCount > 0)
|
||||
ImGui.PopStyleColor(_pushedColorCount);
|
||||
|
||||
_hasPushed = false;
|
||||
_pushedColorCount = 0;
|
||||
_pushedStyleVarCount = 0;
|
||||
}
|
||||
|
||||
private static void Push(ImGuiCol col, Vector4 rgba)
|
||||
{
|
||||
if (rgba.X > 1f || rgba.Y > 1f || rgba.Z > 1f || rgba.W > 1f)
|
||||
rgba /= 255f;
|
||||
|
||||
ImGui.PushStyleColor(col, rgba);
|
||||
_pushedColorCount++;
|
||||
}
|
||||
|
||||
private static void Push(ImGuiCol col, uint packedRgba)
|
||||
{
|
||||
ImGui.PushStyleColor(col, packedRgba);
|
||||
_pushedColorCount++;
|
||||
}
|
||||
|
||||
private static void PushStyleVar(ImGuiStyleVar var, float value)
|
||||
{
|
||||
ImGui.PushStyleVar(var, value);
|
||||
_pushedStyleVarCount++;
|
||||
}
|
||||
|
||||
private static void PushStyleVar(ImGuiStyleVar var, Vector2 value)
|
||||
{
|
||||
ImGui.PushStyleVar(var, value);
|
||||
_pushedStyleVarCount++;
|
||||
}
|
||||
_config = config;
|
||||
_themeConfig = themeConfig;
|
||||
}
|
||||
public static bool ShouldUseTheme => _config?.Current.UseLightlessRedesign ?? false;
|
||||
|
||||
private static bool _hasPushed;
|
||||
private static int _pushedColorCount;
|
||||
private static int _pushedStyleVarCount;
|
||||
|
||||
private static readonly StyleColorOption[] _colorOptions =
|
||||
[
|
||||
new("color.text", "Text", () => Rgba(255, 255, 255, 255), ImGuiCol.Text),
|
||||
new("color.textDisabled", "Text (Disabled)", () => Rgba(128, 128, 128, 255), ImGuiCol.TextDisabled),
|
||||
new("color.windowBg", "Window Background", () => Rgba(23, 23, 23, 248), ImGuiCol.WindowBg),
|
||||
new("color.childBg", "Child Background", () => Rgba(23, 23, 23, 66), ImGuiCol.ChildBg),
|
||||
new("color.popupBg", "Popup Background", () => Rgba(23, 23, 23, 248), ImGuiCol.PopupBg),
|
||||
new("color.border", "Border", () => Rgba(65, 65, 65, 255), ImGuiCol.Border),
|
||||
new("color.borderShadow", "Border Shadow", () => Rgba(0, 0, 0, 150), ImGuiCol.BorderShadow),
|
||||
new("color.frameBg", "Frame Background", () => Rgba(40, 40, 40, 255), ImGuiCol.FrameBg),
|
||||
new("color.frameBgHovered", "Frame Background (Hover)", () => Rgba(50, 50, 50, 255), ImGuiCol.FrameBgHovered),
|
||||
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.titleBgActive", "Title Background (Active)", () => Rgba(30, 30, 30, 255), ImGuiCol.TitleBgActive),
|
||||
new("color.titleBgCollapsed", "Title Background (Collapsed)", () => Rgba(27, 27, 27, 255), ImGuiCol.TitleBgCollapsed),
|
||||
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.scrollbarGrab", "Scrollbar Grab", () => Rgba(62, 62, 62, 255), ImGuiCol.ScrollbarGrab),
|
||||
new("color.scrollbarGrabHovered", "Scrollbar Grab (Hover)", () => Rgba(70, 70, 70, 255), ImGuiCol.ScrollbarGrabHovered),
|
||||
new("color.scrollbarGrabActive", "Scrollbar Grab (Active)", () => Rgba(70, 70, 70, 255), ImGuiCol.ScrollbarGrabActive),
|
||||
new("color.checkMark", "Check Mark", () => UIColors.Get("LightlessPurple"), ImGuiCol.CheckMark, UiColorKey: "LightlessPurple"),
|
||||
new("color.sliderGrab", "Slider Grab", () => Rgba(101, 101, 101, 255), ImGuiCol.SliderGrab),
|
||||
new("color.sliderGrabActive", "Slider Grab (Active)", () => Rgba(123, 123, 123, 255), ImGuiCol.SliderGrabActive),
|
||||
new("color.button", "Button", () => UIColors.Get("ButtonDefault"), ImGuiCol.Button, UiColorKey: "ButtonDefault"),
|
||||
new("color.buttonHovered", "Button (Hover)", () => UIColors.Get("LightlessPurple"), ImGuiCol.ButtonHovered, UiColorKey: "LightlessPurple"),
|
||||
new("color.buttonActive", "Button (Active)", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.ButtonActive, UiColorKey: "LightlessPurpleActive"),
|
||||
new("color.header", "Header", () => Rgba(0, 0, 0, 60), ImGuiCol.Header),
|
||||
new("color.headerHovered", "Header (Hover)", () => Rgba(0, 0, 0, 90), ImGuiCol.HeaderHovered),
|
||||
new("color.headerActive", "Header (Active)", () => Rgba(0, 0, 0, 120), ImGuiCol.HeaderActive),
|
||||
new("color.separator", "Separator", () => Rgba(75, 75, 75, 121), ImGuiCol.Separator),
|
||||
new("color.separatorHovered", "Separator (Hover)", () => UIColors.Get("LightlessPurple"), ImGuiCol.SeparatorHovered, UiColorKey: "LightlessPurple"),
|
||||
new("color.separatorActive", "Separator (Active)", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.SeparatorActive, UiColorKey: "LightlessPurpleActive"),
|
||||
new("color.resizeGrip", "Resize Grip", () => Rgba(0, 0, 0, 0), ImGuiCol.ResizeGrip),
|
||||
new("color.resizeGripHovered", "Resize Grip (Hover)", () => Rgba(0, 0, 0, 0), ImGuiCol.ResizeGripHovered),
|
||||
new("color.resizeGripActive", "Resize Grip (Active)", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.ResizeGripActive, UiColorKey: "LightlessPurpleActive"),
|
||||
new("color.tab", "Tab", () => Rgba(40, 40, 40, 255), ImGuiCol.Tab),
|
||||
new("color.tabHovered", "Tab (Hover)", () => UIColors.Get("LightlessPurple"), ImGuiCol.TabHovered, UiColorKey: "LightlessPurple"),
|
||||
new("color.tabActive", "Tab (Active)", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.TabActive, UiColorKey: "LightlessPurpleActive"),
|
||||
new("color.tabUnfocused", "Tab (Unfocused)", () => Rgba(40, 40, 40, 255), ImGuiCol.TabUnfocused),
|
||||
new("color.tabUnfocusedActive", "Tab (Unfocused Active)", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.TabUnfocusedActive, UiColorKey: "LightlessPurpleActive"),
|
||||
new("color.dockingPreview", "Docking Preview", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.DockingPreview, UiColorKey: "LightlessPurpleActive"),
|
||||
new("color.dockingEmptyBg", "Docking Empty Background", () => Rgba(50, 50, 50, 255), ImGuiCol.DockingEmptyBg),
|
||||
new("color.plotLines", "Plot Lines", () => Rgba(150, 150, 150, 255), ImGuiCol.PlotLines),
|
||||
new("color.tableHeaderBg", "Table Header Background", () => Rgba(48, 48, 48, 255), ImGuiCol.TableHeaderBg),
|
||||
new("color.tableBorderStrong", "Table Border Strong", () => Rgba(79, 79, 89, 255), ImGuiCol.TableBorderStrong),
|
||||
new("color.tableBorderLight", "Table Border Light", () => Rgba(59, 59, 64, 255), ImGuiCol.TableBorderLight),
|
||||
new("color.tableRowBg", "Table Row Background", () => Rgba(0, 0, 0, 0), ImGuiCol.TableRowBg),
|
||||
new("color.tableRowBgAlt", "Table Row Background (Alt)", () => Rgba(255, 255, 255, 15), ImGuiCol.TableRowBgAlt),
|
||||
new("color.textSelectedBg", "Text Selection Background", () => Rgba(173, 138, 245, 255), ImGuiCol.TextSelectedBg),
|
||||
new("color.dragDropTarget", "Drag & Drop Target", () => Rgba(173, 138, 245, 255), ImGuiCol.DragDropTarget),
|
||||
new("color.navHighlight", "Navigation Highlight", () => Rgba(173, 138, 245, 179), ImGuiCol.NavHighlight),
|
||||
new("color.navWindowingDimBg", "Navigation Window Dim", () => Rgba(204, 204, 204, 51), ImGuiCol.NavWindowingDimBg),
|
||||
new("color.navWindowingHighlight", "Navigation Window Highlight", () => Rgba(204, 204, 204, 89), ImGuiCol.NavWindowingHighlight)
|
||||
];
|
||||
|
||||
private static readonly StyleVector2Option[] _vector2Options =
|
||||
[
|
||||
new("vector.windowPadding", "Window Padding", () => new Vector2(6f, 6f), ImGuiStyleVar.WindowPadding),
|
||||
new("vector.framePadding", "Frame Padding", () => new Vector2(4f, 3f), ImGuiStyleVar.FramePadding),
|
||||
new("vector.cellPadding", "Cell Padding", () => new Vector2(4f, 4f), ImGuiStyleVar.CellPadding),
|
||||
new("vector.itemSpacing", "Item Spacing", () => new Vector2(4f, 4f), ImGuiStyleVar.ItemSpacing),
|
||||
new("vector.itemInnerSpacing", "Item Inner Spacing", () => new Vector2(4f, 4f), ImGuiStyleVar.ItemInnerSpacing)
|
||||
];
|
||||
|
||||
private static readonly StyleFloatOption[] _floatOptions =
|
||||
[
|
||||
new("float.indentSpacing", "Indent Spacing", 21f, ImGuiStyleVar.IndentSpacing, 0f, 100f, 0.5f),
|
||||
new("float.scrollbarSize", "Scrollbar Size", 10f, ImGuiStyleVar.ScrollbarSize, 4f, 30f, 0.5f),
|
||||
new("float.grabMinSize", "Grab Minimum Size", 20f, ImGuiStyleVar.GrabMinSize, 1f, 80f, 0.5f),
|
||||
new("float.windowBorderSize", "Window Border Size", 1.5f, ImGuiStyleVar.WindowBorderSize, 0f, 5f, 0.1f),
|
||||
new("float.childBorderSize", "Child Border Size", 1.5f, ImGuiStyleVar.ChildBorderSize, 0f, 5f, 0.1f),
|
||||
new("float.popupBorderSize", "Popup Border Size", 1.5f, ImGuiStyleVar.PopupBorderSize, 0f, 5f, 0.1f),
|
||||
new("float.frameBorderSize", "Frame Border Size", 0f, ImGuiStyleVar.FrameBorderSize, 0f, 5f, 0.1f),
|
||||
new("float.windowRounding", "Window Rounding", 7f, ImGuiStyleVar.WindowRounding, 0f, 20f, 0.2f),
|
||||
new("float.childRounding", "Child Rounding", 4f, ImGuiStyleVar.ChildRounding, 0f, 20f, 0.2f),
|
||||
new("float.frameRounding", "Frame Rounding", 4f, ImGuiStyleVar.FrameRounding, 0f, 20f, 0.2f),
|
||||
new("float.popupRounding", "Popup Rounding", 4f, ImGuiStyleVar.PopupRounding, 0f, 20f, 0.2f),
|
||||
new("float.scrollbarRounding", "Scrollbar Rounding", 4f, ImGuiStyleVar.ScrollbarRounding, 0f, 20f, 0.2f),
|
||||
new("float.grabRounding", "Grab Rounding", 4f, ImGuiStyleVar.GrabRounding, 0f, 20f, 0.2f),
|
||||
new("float.tabRounding", "Tab Rounding", 4f, ImGuiStyleVar.TabRounding, 0f, 20f, 0.2f)
|
||||
];
|
||||
|
||||
public static IReadOnlyList<StyleColorOption> ColorOptions => _colorOptions;
|
||||
public static IReadOnlyList<StyleFloatOption> FloatOptions => _floatOptions;
|
||||
public static IReadOnlyList<StyleVector2Option> Vector2Options => _vector2Options;
|
||||
|
||||
public static void PushStyle()
|
||||
{
|
||||
if (_hasPushed)
|
||||
PopStyle();
|
||||
|
||||
if (!ShouldUseTheme)
|
||||
{
|
||||
_hasPushed = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_hasPushed = true;
|
||||
_pushedColorCount = 0;
|
||||
_pushedStyleVarCount = 0;
|
||||
|
||||
foreach (var option in _colorOptions)
|
||||
Push(option.Target, ResolveColor(option));
|
||||
|
||||
foreach (var option in _vector2Options)
|
||||
PushStyleVar(option.Target, ResolveVector(option));
|
||||
|
||||
foreach (var option in _floatOptions)
|
||||
PushStyleVar(option.Target, ResolveFloat(option));
|
||||
}
|
||||
|
||||
public static void PopStyle()
|
||||
{
|
||||
if (!_hasPushed)
|
||||
return;
|
||||
|
||||
if (_pushedStyleVarCount > 0)
|
||||
ImGui.PopStyleVar(_pushedStyleVarCount);
|
||||
if (_pushedColorCount > 0)
|
||||
ImGui.PopStyleColor(_pushedColorCount);
|
||||
|
||||
_hasPushed = false;
|
||||
_pushedColorCount = 0;
|
||||
_pushedStyleVarCount = 0;
|
||||
}
|
||||
|
||||
private static Vector4 ResolveColor(StyleColorOption option)
|
||||
{
|
||||
var defaultValue = NormalizeColorVector(option.DefaultValue());
|
||||
if (_themeConfig?.Current.StyleOverrides.TryGetValue(option.Key, out var overrideValue) == true && overrideValue.Color is { } packed)
|
||||
return PackedColorToVector4(packed);
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private static Vector2 ResolveVector(StyleVector2Option option)
|
||||
{
|
||||
var value = option.DefaultValue();
|
||||
if (_themeConfig?.Current.StyleOverrides.TryGetValue(option.Key, out var overrideValue) == true && overrideValue.Vector2 is { } vectorOverride)
|
||||
{
|
||||
value = vectorOverride;
|
||||
}
|
||||
|
||||
if (option.Min is { } min)
|
||||
value = Vector2.Max(value, min);
|
||||
if (option.Max is { } max)
|
||||
value = Vector2.Min(value, max);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static float ResolveFloat(StyleFloatOption option)
|
||||
{
|
||||
var value = option.DefaultValue;
|
||||
if (_themeConfig?.Current.StyleOverrides.TryGetValue(option.Key, out var overrideValue) == true && overrideValue.Float is { } floatOverride)
|
||||
{
|
||||
value = floatOverride;
|
||||
}
|
||||
|
||||
if (option.Min.HasValue)
|
||||
value = MathF.Max(option.Min.Value, value);
|
||||
if (option.Max.HasValue)
|
||||
value = MathF.Min(option.Max.Value, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static void Push(ImGuiCol col, Vector4 rgba)
|
||||
{
|
||||
rgba = NormalizeColorVector(rgba);
|
||||
ImGui.PushStyleColor(col, rgba);
|
||||
_pushedColorCount++;
|
||||
}
|
||||
|
||||
private static void PushStyleVar(ImGuiStyleVar var, float value)
|
||||
{
|
||||
ImGui.PushStyleVar(var, value);
|
||||
_pushedStyleVarCount++;
|
||||
}
|
||||
|
||||
private static void PushStyleVar(ImGuiStyleVar var, Vector2 value)
|
||||
{
|
||||
ImGui.PushStyleVar(var, value);
|
||||
_pushedStyleVarCount++;
|
||||
}
|
||||
|
||||
private static Vector4 Rgba(byte r, byte g, byte b, byte a = 255)
|
||||
=> new Vector4(r / 255f, g / 255f, b / 255f, a / 255f);
|
||||
|
||||
internal static Vector4 NormalizeColorVector(Vector4 rgba)
|
||||
{
|
||||
if (rgba.X > 1f || rgba.Y > 1f || rgba.Z > 1f || rgba.W > 1f)
|
||||
rgba /= 255f;
|
||||
return rgba;
|
||||
}
|
||||
|
||||
internal static Vector4 PackedColorToVector4(uint color)
|
||||
=> new(
|
||||
(color & 0xFF) / 255f,
|
||||
((color >> 8) & 0xFF) / 255f,
|
||||
((color >> 16) & 0xFF) / 255f,
|
||||
((color >> 24) & 0xFF) / 255f);
|
||||
}
|
||||
|
||||
@@ -336,7 +336,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
}
|
||||
ImGui.Separator();
|
||||
|
||||
if (_uiSharedService.MediumTreeNode("Mass Cleanup", UIColors.Get("LightlessPurple")))
|
||||
if (_uiSharedService.MediumTreeNode("Mass Cleanup", UIColors.Get("DimRed")))
|
||||
{
|
||||
using (ImRaii.Disabled(!UiSharedService.CtrlPressed()))
|
||||
{
|
||||
@@ -348,6 +348,18 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
UiSharedService.AttachToolTip("This will remove all non-pinned, non-moderator users from the Syncshell."
|
||||
+ UiSharedService.TooltipSeparator + "Hold CTRL to enable this button");
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
using (ImRaii.Disabled(!UiSharedService.CtrlPressed()))
|
||||
{
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Brush, "Clear Lightfinder Users"))
|
||||
{
|
||||
_ = _apiController.GroupClearFinder(new(GroupFullInfo.Group));
|
||||
}
|
||||
}
|
||||
UiSharedService.AttachToolTip("This will remove all users that joined through Lightfinder from the Syncshell."
|
||||
+ UiSharedService.TooltipSeparator + "Hold CTRL to enable this button");
|
||||
|
||||
ImGuiHelpers.ScaledDummy(2f);
|
||||
ImGui.Separator();
|
||||
ImGuiHelpers.ScaledDummy(2f);
|
||||
@@ -410,12 +422,12 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
UiSharedService.TextWrapped($"Syncshell was pruned and {_pruneTask.Result} inactive user(s) have been removed.");
|
||||
}
|
||||
}
|
||||
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
|
||||
_uiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 1.5f);
|
||||
ImGui.TreePop();
|
||||
}
|
||||
ImGui.Separator();
|
||||
|
||||
if (_uiSharedService.MediumTreeNode("User Bans", UIColors.Get("LightlessPurple")))
|
||||
if (_uiSharedService.MediumTreeNode("User Bans", UIColors.Get("LightlessYellow")))
|
||||
{
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Retweet, "Refresh Banlist from Server"))
|
||||
{
|
||||
@@ -456,7 +468,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
}
|
||||
ImGui.EndTable();
|
||||
}
|
||||
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
|
||||
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f);
|
||||
ImGui.TreePop();
|
||||
}
|
||||
ImGui.Separator();
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.API.Dto;
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.WebAPI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using System.Numerics;
|
||||
|
||||
namespace LightlessSync.UI;
|
||||
@@ -20,13 +20,16 @@ namespace LightlessSync.UI;
|
||||
public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
{
|
||||
private readonly ApiController _apiController;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly BroadcastService _broadcastService;
|
||||
private readonly UiSharedService _uiSharedService;
|
||||
private readonly BroadcastScannerService _broadcastScannerService;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
|
||||
private readonly List<GroupJoinDto> _nearbySyncshells = new();
|
||||
private readonly List<GroupJoinDto> _nearbySyncshells = [];
|
||||
private List<GroupFullInfoDto> _currentSyncshells = [];
|
||||
private int _selectedNearbyIndex = -1;
|
||||
private readonly HashSet<string> _recentlyJoined = new(StringComparer.Ordinal);
|
||||
|
||||
private GroupJoinDto? _joinDto;
|
||||
private GroupJoinInfoDto? _joinInfo;
|
||||
@@ -37,17 +40,18 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
LightlessMediator mediator,
|
||||
PerformanceCollectorService performanceCollectorService,
|
||||
BroadcastService broadcastService,
|
||||
LightlessConfigService configService,
|
||||
UiSharedService uiShared,
|
||||
ApiController apiController,
|
||||
BroadcastScannerService broadcastScannerService
|
||||
) : base(logger, mediator, "Shellfinder###LightlessSyncshellFinderUI", performanceCollectorService)
|
||||
BroadcastScannerService broadcastScannerService,
|
||||
PairManager pairManager,
|
||||
DalamudUtilService dalamudUtilService) : base(logger, mediator, "Shellfinder###LightlessSyncshellFinderUI", performanceCollectorService)
|
||||
{
|
||||
_broadcastService = broadcastService;
|
||||
_uiSharedService = uiShared;
|
||||
_configService = configService;
|
||||
_apiController = apiController;
|
||||
_broadcastScannerService = broadcastScannerService;
|
||||
_pairManager = pairManager;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
|
||||
IsOpen = false;
|
||||
SizeConstraints = new()
|
||||
@@ -56,14 +60,14 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
MaximumSize = new(600, 550)
|
||||
};
|
||||
|
||||
Mediator.Subscribe<SyncshellBroadcastsUpdatedMessage>(this, async _ => await RefreshSyncshellsAsync());
|
||||
Mediator.Subscribe<BroadcastStatusChangedMessage>(this, async _ => await RefreshSyncshellsAsync());
|
||||
Mediator.Subscribe<SyncshellBroadcastsUpdatedMessage>(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false));
|
||||
Mediator.Subscribe<BroadcastStatusChangedMessage>(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false));
|
||||
}
|
||||
|
||||
public override async void OnOpen()
|
||||
{
|
||||
_ownPermissions = _apiController.DefaultPermissions.DeepClone()!;
|
||||
await RefreshSyncshellsAsync();
|
||||
await RefreshSyncshellsAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected override void DrawInternal()
|
||||
@@ -100,22 +104,45 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
return;
|
||||
}
|
||||
|
||||
DrawSyncshellTable();
|
||||
|
||||
if (_joinDto != null && _joinInfo != null && _joinInfo.Success)
|
||||
DrawConfirmation();
|
||||
}
|
||||
|
||||
private void DrawSyncshellTable()
|
||||
{
|
||||
if (ImGui.BeginTable("##NearbySyncshellsTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg))
|
||||
{
|
||||
ImGui.TableSetupColumn("Alias", ImGuiTableColumnFlags.WidthStretch);
|
||||
ImGui.TableSetupColumn("GID", ImGuiTableColumnFlags.WidthStretch);
|
||||
ImGui.TableSetupColumn("Syncshell", ImGuiTableColumnFlags.WidthStretch);
|
||||
ImGui.TableSetupColumn("Broadcaster", ImGuiTableColumnFlags.WidthStretch);
|
||||
ImGui.TableSetupColumn("Join", ImGuiTableColumnFlags.WidthFixed, 80f * ImGuiHelpers.GlobalScale);
|
||||
ImGui.TableHeadersRow();
|
||||
|
||||
for (int i = 0; i < _nearbySyncshells.Count; i++)
|
||||
foreach (var shell in _nearbySyncshells)
|
||||
{
|
||||
var shell = _nearbySyncshells[i];
|
||||
// Check if there is an active broadcast for this syncshell, if not, skipping this syncshell
|
||||
var broadcast = _broadcastScannerService.GetActiveSyncshellBroadcasts()
|
||||
.FirstOrDefault(b => string.Equals(b.GID, shell.Group.GID, StringComparison.Ordinal));
|
||||
|
||||
if (broadcast == null)
|
||||
continue; // no active broadcasts
|
||||
|
||||
var (Name, Address) = _dalamudUtilService.FindPlayerByNameHash(broadcast.HashedCID);
|
||||
if (string.IsNullOrEmpty(Name))
|
||||
continue; // broadcaster not found in area, skipping
|
||||
|
||||
ImGui.TableNextRow();
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(shell.Group.Alias ?? "(No Alias)");
|
||||
|
||||
var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID;
|
||||
ImGui.TextUnformatted(displayName);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(shell.Group.GID);
|
||||
var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(Address);
|
||||
var broadcasterName = !string.IsNullOrEmpty(worldName) ? $"{Name} ({worldName})" : Name;
|
||||
ImGui.TextUnformatted(broadcasterName);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
|
||||
var label = $"Join##{shell.Group.GID}";
|
||||
@@ -123,75 +150,90 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f));
|
||||
ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessGreen").WithAlpha(0.75f));
|
||||
|
||||
if (ImGui.Button(label))
|
||||
var isAlreadyMember = _currentSyncshells.Exists(g => string.Equals(g.GID, shell.GID, StringComparison.Ordinal));
|
||||
var isRecentlyJoined = _recentlyJoined.Contains(shell.GID);
|
||||
|
||||
if (!isAlreadyMember && !isRecentlyJoined)
|
||||
{
|
||||
_logger.LogInformation($"Join requested for Syncshell {shell.Group.GID} ({shell.Group.Alias})");
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
if (ImGui.Button(label))
|
||||
{
|
||||
try
|
||||
{
|
||||
var info = await _apiController.GroupJoinHashed(new GroupJoinHashedDto(
|
||||
shell.Group,
|
||||
shell.Password,
|
||||
shell.GroupUserPreferredPermissions
|
||||
)).ConfigureAwait(false);
|
||||
_logger.LogInformation($"Join requested for Syncshell {shell.Group.GID} ({shell.Group.Alias})");
|
||||
|
||||
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)
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
_logger.LogError(ex, $"Join failed for {shell.Group.GID}");
|
||||
}
|
||||
});
|
||||
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
|
||||
{
|
||||
using (ImRaii.Disabled())
|
||||
{
|
||||
ImGui.Button(label);
|
||||
}
|
||||
UiSharedService.AttachToolTip("Already a member or owner of this Syncshell.");
|
||||
}
|
||||
|
||||
|
||||
ImGui.PopStyleColor(3);
|
||||
}
|
||||
|
||||
ImGui.EndTable();
|
||||
}
|
||||
|
||||
if (_joinDto != null && _joinInfo != null && _joinInfo.Success)
|
||||
DrawConfirmation();
|
||||
}
|
||||
|
||||
private void DrawConfirmation()
|
||||
{
|
||||
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}"))
|
||||
if (_joinDto != null && _joinInfo != null)
|
||||
{
|
||||
var finalPermissions = GroupUserPreferredPermissions.NoneSet;
|
||||
finalPermissions.SetDisableSounds(_ownPermissions.DisableGroupSounds);
|
||||
finalPermissions.SetDisableAnimations(_ownPermissions.DisableGroupAnimations);
|
||||
finalPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX);
|
||||
ImGui.Separator();
|
||||
ImGui.TextUnformatted($"Join Syncshell: {_joinDto.Group.AliasOrGID} by {_joinInfo.OwnerAliasOrUID}");
|
||||
ImGuiHelpers.ScaledDummy(2f);
|
||||
ImGui.TextUnformatted("Suggested Syncshell Permissions:");
|
||||
|
||||
_ = _apiController.GroupJoinFinalize(new GroupJoinDto(_joinDto.Group, _joinDto.Password, finalPermissions));
|
||||
_joinDto = null;
|
||||
_joinInfo = null;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,6 +266,9 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
private async Task RefreshSyncshellsAsync()
|
||||
{
|
||||
var syncshellBroadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts();
|
||||
_currentSyncshells = [.. _pairManager.GroupPairs.Select(g => g.Key)];
|
||||
|
||||
_recentlyJoined.RemoveWhere(gid => _currentSyncshells.Any(s => string.Equals(s.GID, gid, StringComparison.Ordinal)));
|
||||
|
||||
if (syncshellBroadcasts.Count == 0)
|
||||
{
|
||||
@@ -231,11 +276,11 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
return;
|
||||
}
|
||||
|
||||
List<GroupJoinDto> updatedList;
|
||||
List<GroupJoinDto>? updatedList = [];
|
||||
try
|
||||
{
|
||||
var groups = await _apiController.GetBroadcastedGroups(syncshellBroadcasts);
|
||||
updatedList = groups?.ToList() ?? new();
|
||||
var groups = await _apiController.GetBroadcastedGroups(syncshellBroadcasts).ConfigureAwait(false);
|
||||
updatedList = groups?.ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -243,24 +288,23 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
return;
|
||||
}
|
||||
|
||||
var currentGids = _nearbySyncshells.Select(s => s.Group.GID).ToHashSet();
|
||||
var newGids = updatedList.Select(s => s.Group.GID).ToHashSet();
|
||||
var currentGids = _nearbySyncshells.Select(s => s.Group.GID).ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
if (currentGids.SetEquals(newGids))
|
||||
return;
|
||||
|
||||
var previousGid = GetSelectedGid();
|
||||
|
||||
_nearbySyncshells.Clear();
|
||||
_nearbySyncshells.AddRange(updatedList);
|
||||
|
||||
if (previousGid != null)
|
||||
if (updatedList != null)
|
||||
{
|
||||
var newIndex = _nearbySyncshells.FindIndex(s => s.Group.GID == previousGid);
|
||||
if (newIndex >= 0)
|
||||
var previousGid = GetSelectedGid();
|
||||
|
||||
_nearbySyncshells.Clear();
|
||||
_nearbySyncshells.AddRange(updatedList);
|
||||
|
||||
if (previousGid != null)
|
||||
{
|
||||
_selectedNearbyIndex = newIndex;
|
||||
return;
|
||||
var newIndex = _nearbySyncshells.FindIndex(s => string.Equals(s.Group.GID, previousGid, StringComparison.Ordinal));
|
||||
if (newIndex >= 0)
|
||||
{
|
||||
_selectedNearbyIndex = newIndex;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,8 +335,4 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
return _nearbySyncshells[_selectedNearbyIndex].Group.GID;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Utility;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.WebAPI;
|
||||
using System.Numerics;
|
||||
|
||||
|
||||
namespace LightlessSync.UI;
|
||||
|
||||
public class TopTabMenu
|
||||
@@ -19,19 +23,28 @@ public class TopTabMenu
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly PairRequestService _pairRequestService;
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private readonly HashSet<string> _pendingPairRequestActions = new(StringComparer.Ordinal);
|
||||
private bool _pairRequestsExpanded; // useless for now
|
||||
private int _lastRequestCount;
|
||||
private readonly UiSharedService _uiSharedService;
|
||||
private readonly NotificationService _lightlessNotificationService;
|
||||
private string _filter = string.Empty;
|
||||
private int _globalControlCountdown = 0;
|
||||
|
||||
private float _pairRequestsHeight = 150f;
|
||||
private string _pairToAdd = string.Empty;
|
||||
|
||||
private SelectedTab _selectedTab = SelectedTab.None;
|
||||
public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService)
|
||||
public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService)
|
||||
{
|
||||
_lightlessMediator = lightlessMediator;
|
||||
_apiController = apiController;
|
||||
_pairManager = pairManager;
|
||||
_pairRequestService = pairRequestService;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
_uiSharedService = uiSharedService;
|
||||
_lightlessNotificationService = lightlessNotificationService;
|
||||
}
|
||||
|
||||
private enum SelectedTab
|
||||
@@ -40,7 +53,8 @@ public class TopTabMenu
|
||||
Individual,
|
||||
Syncshell,
|
||||
Lightfinder,
|
||||
UserConfig
|
||||
UserConfig,
|
||||
Settings
|
||||
}
|
||||
|
||||
public string Filter
|
||||
@@ -67,7 +81,7 @@ public class TopTabMenu
|
||||
{
|
||||
var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
|
||||
var spacing = ImGui.GetStyle().ItemSpacing;
|
||||
var buttonX = (availableWidth - (spacing.X * 3)) / 4f;
|
||||
var buttonX = (availableWidth - (spacing.X * 4)) / 5f;
|
||||
var buttonY = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Pause).Y;
|
||||
var buttonSize = new Vector2(buttonX, buttonY);
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
@@ -144,6 +158,18 @@ public class TopTabMenu
|
||||
}
|
||||
UiSharedService.AttachToolTip("Your User Menu");
|
||||
|
||||
ImGui.SameLine();
|
||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||
{
|
||||
var x = ImGui.GetCursorScreenPos();
|
||||
if (ImGui.Button(FontAwesomeIcon.Cog.ToIconString(), buttonSize))
|
||||
{
|
||||
_lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
|
||||
}
|
||||
ImGui.SameLine();
|
||||
}
|
||||
UiSharedService.AttachToolTip("Open Lightless Settings");
|
||||
|
||||
ImGui.NewLine();
|
||||
btncolor.Dispose();
|
||||
|
||||
@@ -169,6 +195,85 @@ public class TopTabMenu
|
||||
}
|
||||
|
||||
if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f);
|
||||
|
||||
#if DEBUG
|
||||
if (ImGui.Button("Test Pair Request"))
|
||||
{
|
||||
_lightlessNotificationService.ShowPairRequestNotification(
|
||||
"Debug User",
|
||||
"debug-user-id",
|
||||
onAccept: () =>
|
||||
{
|
||||
_lightlessMediator.Publish(new NotificationMessage(
|
||||
"Pair Accepted",
|
||||
"Debug pair request was accepted!",
|
||||
NotificationType.Info,
|
||||
TimeSpan.FromSeconds(3)));
|
||||
},
|
||||
onDecline: () =>
|
||||
{
|
||||
_lightlessMediator.Publish(new NotificationMessage(
|
||||
"Pair Declined",
|
||||
"Debug pair request was declined.",
|
||||
NotificationType.Warning,
|
||||
TimeSpan.FromSeconds(3)));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Test Info"))
|
||||
{
|
||||
_lightlessMediator.Publish(new NotificationMessage(
|
||||
"Information",
|
||||
"This is a test ifno notification with some longer text to see how it wraps. This is a test ifno notification with some longer text to see how it wraps. This is a test ifno notification with some longer text to see how it wraps. This is a test ifno notification with some longer text to see how it wraps.",
|
||||
NotificationType.Info,
|
||||
TimeSpan.FromSeconds(5)));
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Test Warning"))
|
||||
{
|
||||
_lightlessMediator.Publish(new NotificationMessage(
|
||||
"Warning",
|
||||
"This is a test warning notification.",
|
||||
NotificationType.Warning,
|
||||
TimeSpan.FromSeconds(7)));
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Test Error"))
|
||||
{
|
||||
_lightlessMediator.Publish(new NotificationMessage(
|
||||
"Error",
|
||||
"This is a test error notification erp police",
|
||||
NotificationType.Error,
|
||||
TimeSpan.FromSeconds(10)));
|
||||
}
|
||||
|
||||
if (ImGui.Button("Test Download Progress"))
|
||||
{
|
||||
var downloadStatus = new List<(string playerName, float progress, string status)>
|
||||
{
|
||||
("Mauwmauw Nekochan", 0.85f, "downloading"),
|
||||
("Raelynn Kitsune", 0.34f, "downloading"),
|
||||
("Jaina Elraeth", 0.67f, "downloading"),
|
||||
("Vaelstra Bloodthorn", 0.19f, "downloading"),
|
||||
("Lydia Hera Moondrop", 0.86f, "downloading"),
|
||||
("C'liina Star", 1.0f, "completed")
|
||||
};
|
||||
|
||||
_lightlessNotificationService.ShowPairDownloadNotification(downloadStatus);
|
||||
}
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Dismiss Download"))
|
||||
{
|
||||
_lightlessNotificationService.DismissPairDownloadNotification();
|
||||
}
|
||||
#endif
|
||||
|
||||
DrawIncomingPairRequests(availableWidth);
|
||||
|
||||
ImGui.Separator();
|
||||
|
||||
DrawFilter(availableWidth, spacing.X);
|
||||
@@ -192,6 +297,207 @@ public class TopTabMenu
|
||||
UiSharedService.AttachToolTip("Pair with " + (_pairToAdd.IsNullOrEmpty() ? "other user" : _pairToAdd));
|
||||
}
|
||||
|
||||
private void DrawIncomingPairRequests(float availableWidth)
|
||||
{
|
||||
var requests = _pairRequestService.GetActiveRequests();
|
||||
var count = requests.Count;
|
||||
if (count == 0)
|
||||
{
|
||||
_pairRequestsExpanded = false;
|
||||
_lastRequestCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (count > _lastRequestCount)
|
||||
{
|
||||
_pairRequestsExpanded = true;
|
||||
}
|
||||
_lastRequestCount = count;
|
||||
|
||||
var label = $"Incoming Pair Requests - {count}##IncomingPairRequestsHeader";
|
||||
|
||||
using (ImRaii.PushColor(ImGuiCol.Header, UIColors.Get("LightlessPurple")))
|
||||
using (ImRaii.PushColor(ImGuiCol.HeaderHovered, UIColors.Get("LightlessPurpleActive")))
|
||||
using (ImRaii.PushColor(ImGuiCol.HeaderActive, UIColors.Get("LightlessPurple")))
|
||||
{
|
||||
bool open = ImGui.CollapsingHeader(label, ImGuiTreeNodeFlags.DefaultOpen);
|
||||
_pairRequestsExpanded = open;
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
UiSharedService.AttachToolTip("Expand to view incoming pair requests.");
|
||||
|
||||
if (open)
|
||||
{
|
||||
var lineHeight = ImGui.GetTextLineHeightWithSpacing();
|
||||
//var desiredHeight = Math.Clamp(count * lineHeight * 2f, 130f * ImGuiHelpers.GlobalScale, 185f * ImGuiHelpers.GlobalScale); we use resize bar instead
|
||||
|
||||
ImGui.SetCursorPosX(ImGui.GetCursorPosX() - 2f);
|
||||
|
||||
using (ImRaii.PushColor(ImGuiCol.ChildBg, UIColors.Get("LightlessPurple")))
|
||||
using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 6f))
|
||||
{
|
||||
if (ImGui.BeginChild("##IncomingPairRequestsOuter", new Vector2(availableWidth + 5f, _pairRequestsHeight), true))
|
||||
{
|
||||
var defaultChildBg = ImGui.GetStyle().Colors[(int)ImGuiCol.WindowBg];
|
||||
using (ImRaii.PushColor(ImGuiCol.ChildBg, defaultChildBg))
|
||||
using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 5f))
|
||||
using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(6, 6)))
|
||||
{
|
||||
if (ImGui.BeginChild("##IncomingPairRequestsInner", new Vector2(0, 0), true))
|
||||
{
|
||||
using (ImRaii.PushColor(ImGuiCol.TableBorderStrong, ImGui.GetStyle().Colors[(int)ImGuiCol.Border]))
|
||||
using (ImRaii.PushColor(ImGuiCol.TableBorderLight, ImGui.GetStyle().Colors[(int)ImGuiCol.Border]))
|
||||
{
|
||||
DrawPairRequestList(requests);
|
||||
}
|
||||
}
|
||||
ImGui.EndChild();
|
||||
}
|
||||
}
|
||||
ImGui.EndChild();
|
||||
|
||||
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("ButtonDefault"));
|
||||
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurple"));
|
||||
ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessPurpleActive"));
|
||||
ImGui.Button("##resizeHandle", new Vector2(availableWidth, 4f));
|
||||
ImGui.PopStyleColor(3);
|
||||
|
||||
if (ImGui.IsItemActive())
|
||||
{
|
||||
_pairRequestsHeight += ImGui.GetIO().MouseDelta.Y;
|
||||
_pairRequestsHeight = Math.Clamp(_pairRequestsHeight, 100f, 300f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawPairRequestList(IReadOnlyList<PairRequestService.PairRequestDisplay> requests)
|
||||
{
|
||||
float playerColWidth = 207f * ImGuiHelpers.GlobalScale;
|
||||
float receivedColWidth = 73f * ImGuiHelpers.GlobalScale;
|
||||
float actionsColWidth = 50f * ImGuiHelpers.GlobalScale;
|
||||
|
||||
ImGui.Separator();
|
||||
ImGui.TextUnformatted("Player");
|
||||
ImGui.SameLine(playerColWidth + 2f);
|
||||
ImGui.TextUnformatted("Received");
|
||||
ImGui.SameLine(playerColWidth + receivedColWidth + 12f);
|
||||
ImGui.TextUnformatted("Actions");
|
||||
ImGui.Separator();
|
||||
|
||||
foreach (var request in requests)
|
||||
{
|
||||
ImGui.BeginGroup();
|
||||
|
||||
var label = string.IsNullOrEmpty(request.DisplayName) ? request.HashedCid : request.DisplayName;
|
||||
|
||||
ImGui.TextUnformatted(label.Truncate(26));
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
ImGui.TextUnformatted(label);
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
|
||||
ImGui.SameLine(playerColWidth);
|
||||
|
||||
ImGui.TextUnformatted(GetRelativeTime(request.ReceivedAt));
|
||||
ImGui.SameLine(playerColWidth + receivedColWidth);
|
||||
|
||||
DrawPairRequestActions(request);
|
||||
|
||||
ImGui.EndGroup();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawPairRequestActions(PairRequestService.PairRequestDisplay request)
|
||||
{
|
||||
using var id = ImRaii.PushId(request.HashedCid);
|
||||
var label = string.IsNullOrEmpty(request.DisplayName) ? request.HashedCid : request.DisplayName;
|
||||
var inFlight = _pendingPairRequestActions.Contains(request.HashedCid);
|
||||
using (ImRaii.Disabled(inFlight))
|
||||
{
|
||||
if (_uiSharedService.IconButton(FontAwesomeIcon.Check))
|
||||
{
|
||||
_ = AcceptPairRequestAsync(request);
|
||||
}
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip("Accept request");
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (_uiSharedService.IconButton(FontAwesomeIcon.Times))
|
||||
{
|
||||
RejectPairRequest(request.HashedCid, label);
|
||||
}
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip("Decline request");
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetRelativeTime(DateTime receivedAt)
|
||||
{
|
||||
var delta = DateTime.UtcNow - receivedAt;
|
||||
if (delta <= TimeSpan.FromSeconds(10))
|
||||
{
|
||||
return "Just now";
|
||||
}
|
||||
|
||||
if (delta.TotalMinutes >= 1)
|
||||
{
|
||||
return $"{Math.Floor(delta.TotalMinutes)}m {delta.Seconds:D2}s ago";
|
||||
}
|
||||
|
||||
return $"{delta.Seconds}s ago";
|
||||
}
|
||||
|
||||
private async Task AcceptPairRequestAsync(PairRequestService.PairRequestDisplay request)
|
||||
{
|
||||
if (!_pendingPairRequestActions.Add(request.HashedCid))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var myCidHash = (await _dalamudUtilService.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
|
||||
await _apiController.TryPairWithContentId(request.HashedCid).ConfigureAwait(false);
|
||||
_pairRequestService.RemoveRequest(request.HashedCid);
|
||||
|
||||
var display = string.IsNullOrEmpty(request.DisplayName) ? request.HashedCid : request.DisplayName;
|
||||
_lightlessMediator.Publish(new NotificationMessage(
|
||||
"Pair request accepted",
|
||||
$"Sent a pair request back to {display}.",
|
||||
NotificationType.Info,
|
||||
TimeSpan.FromSeconds(3)));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_lightlessMediator.Publish(new NotificationMessage(
|
||||
"Failed to accept pair request",
|
||||
ex.Message,
|
||||
NotificationType.Error,
|
||||
TimeSpan.FromSeconds(5)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
_pendingPairRequestActions.Remove(request.HashedCid);
|
||||
}
|
||||
}
|
||||
|
||||
private void RejectPairRequest(string hashedCid, string playerName)
|
||||
{
|
||||
if (!_pairRequestService.RemoveRequest(hashedCid))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lightlessMediator.Publish(new NotificationMessage("Pair request declined", "Declined " + playerName + "'s pending pair request.",
|
||||
NotificationType.Info,
|
||||
TimeSpan.FromSeconds(3)));
|
||||
}
|
||||
|
||||
private void DrawFilter(float availableWidth, float spacingX)
|
||||
{
|
||||
var buttonSize = _uiSharedService.GetIconTextButtonSize(FontAwesomeIcon.Ban, "Clear");
|
||||
@@ -605,4 +911,4 @@ public class TopTabMenu
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,9 @@ namespace LightlessSync.UI
|
||||
{ "LightlessAdminGlow", "#b09343" },
|
||||
{ "LightlessModeratorText", "#94ffda" },
|
||||
{ "LightlessModeratorGlow", "#599c84" },
|
||||
|
||||
{ "Lightfinder", "#ad8af5" },
|
||||
{ "LightfinderEdge", "#000000" },
|
||||
};
|
||||
|
||||
private static LightlessConfigService? _configService;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.GameFonts;
|
||||
@@ -7,6 +7,7 @@ using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using System;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
@@ -173,12 +174,14 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
|
||||
public static string ByteToString(long bytes, bool addSuffix = true)
|
||||
{
|
||||
string[] suffix = ["B", "KiB", "MiB", "GiB", "TiB"];
|
||||
int i;
|
||||
string[] suffix = { "B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"};
|
||||
int i = 0;
|
||||
double dblSByte = bytes;
|
||||
for (i = 0; i < suffix.Length && bytes >= 1024; i++, bytes /= 1024)
|
||||
|
||||
while (dblSByte >= 1000 && i < suffix.Length - 1)
|
||||
{
|
||||
dblSByte = bytes / 1024.0;
|
||||
dblSByte /= 1000.0;
|
||||
i++;
|
||||
}
|
||||
|
||||
return addSuffix ? $"{dblSByte:0.00} {suffix[i]}" : $"{dblSByte:0.00}";
|
||||
@@ -510,10 +513,71 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
ImGui.Dummy(new Vector2(0, thickness * scale));
|
||||
}
|
||||
|
||||
public static bool CheckboxWithBorder(string label, ref bool value, Vector4? borderColor = null, float borderThickness = 1.0f, float rounding = 3.0f)
|
||||
{
|
||||
var pos = ImGui.GetCursorScreenPos();
|
||||
|
||||
bool changed = ImGui.Checkbox(label, ref value);
|
||||
|
||||
var min = pos;
|
||||
var max = ImGui.GetItemRectMax();
|
||||
|
||||
var col = ImGui.GetColorU32(borderColor ?? ImGuiColors.DalamudGrey);
|
||||
ImGui.GetWindowDrawList().AddRect(min, max, col, rounding, ImDrawFlags.None, borderThickness);
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
public void MediumText(string text, Vector4? color = null)
|
||||
{
|
||||
FontText(text, MediumFont, color);
|
||||
}
|
||||
public void DrawNoteLine(string icon, Vector4 color, string text)
|
||||
{
|
||||
MediumText(icon, color);
|
||||
var iconHeight = ImGui.GetItemRectSize().Y;
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
float textHeight = ImGui.GetTextLineHeight();
|
||||
float offset = (iconHeight - textHeight) * 0.5f;
|
||||
if (offset > 0)
|
||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + offset);
|
||||
|
||||
ImGui.BeginGroup();
|
||||
ImGui.TextWrapped(text);
|
||||
ImGui.EndGroup();
|
||||
}
|
||||
|
||||
public void DrawNoteLine(string icon, Vector4 color, ReadOnlySpan<SeStringUtils.RichTextEntry> fragments)
|
||||
{
|
||||
if (fragments.Length == 0)
|
||||
{
|
||||
DrawNoteLine(icon, color, string.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
MediumText(icon, color);
|
||||
var iconHeight = ImGui.GetItemRectSize().Y;
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
float textHeight = ImGui.GetTextLineHeight();
|
||||
float offset = (iconHeight - textHeight) * 0.5f;
|
||||
if (offset > 0)
|
||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + offset);
|
||||
|
||||
var wrapWidth = ImGui.GetContentRegionAvail().X;
|
||||
ImGui.BeginGroup();
|
||||
var richText = SeStringUtils.BuildRichText(fragments);
|
||||
SeStringUtils.RenderSeStringWrapped(richText, wrapWidth);
|
||||
ImGui.EndGroup();
|
||||
}
|
||||
|
||||
public void DrawNoteLine(string icon, Vector4 color, params SeStringUtils.RichTextEntry[] fragments)
|
||||
{
|
||||
DrawNoteLine(icon, color, fragments.AsSpan());
|
||||
}
|
||||
|
||||
public bool MediumTreeNode(string label, Vector4? textColor = null, float lineWidth = 2f, ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags.SpanAvailWidth)
|
||||
{
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.ImGuiSeStringRenderer;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Lumina.Text;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using DalamudSeString = Dalamud.Game.Text.SeStringHandling.SeString;
|
||||
using DalamudSeStringBuilder = Dalamud.Game.Text.SeStringHandling.SeStringBuilder;
|
||||
using LuminaSeStringBuilder = Lumina.Text.SeStringBuilder;
|
||||
|
||||
namespace LightlessSync.Utils;
|
||||
|
||||
public static class SeStringUtils
|
||||
{
|
||||
public static SeString BuildFormattedPlayerName(string text, Vector4? textColor, Vector4? glowColor)
|
||||
public static DalamudSeString BuildFormattedPlayerName(string text, Vector4? textColor, Vector4? glowColor)
|
||||
{
|
||||
var b = new SeStringBuilder();
|
||||
var b = new DalamudSeStringBuilder();
|
||||
|
||||
if (glowColor is Vector4 glow)
|
||||
b.Add(new GlowPayload(glow));
|
||||
@@ -30,14 +36,47 @@ public static class SeStringUtils
|
||||
return b.Build();
|
||||
}
|
||||
|
||||
public static SeString BuildPlain(string text)
|
||||
public static DalamudSeString BuildPlain(string text)
|
||||
{
|
||||
var b = new SeStringBuilder();
|
||||
var b = new DalamudSeStringBuilder();
|
||||
b.AddText(text ?? string.Empty);
|
||||
return b.Build();
|
||||
}
|
||||
|
||||
public static void RenderSeString(SeString seString, Vector2 position, ImFontPtr? font = null, ImDrawListPtr? drawList = null)
|
||||
public static DalamudSeString BuildRichText(ReadOnlySpan<RichTextEntry> fragments)
|
||||
{
|
||||
var builder = new LuminaSeStringBuilder();
|
||||
|
||||
foreach (var fragment in fragments)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fragment.Text))
|
||||
continue;
|
||||
|
||||
var hasColor = fragment.Color.HasValue;
|
||||
Vector4 color = default;
|
||||
if (hasColor)
|
||||
{
|
||||
color = fragment.Color!.Value;
|
||||
builder.PushColorRgba(color);
|
||||
}
|
||||
|
||||
if (fragment.Bold)
|
||||
builder.AppendSetBold(true);
|
||||
|
||||
builder.Append(fragment.Text.AsSpan());
|
||||
|
||||
if (fragment.Bold)
|
||||
builder.AppendSetBold(false);
|
||||
|
||||
if (hasColor)
|
||||
builder.PopColor();
|
||||
}
|
||||
|
||||
return DalamudSeString.Parse(builder.ToArray());
|
||||
}
|
||||
|
||||
public static DalamudSeString BuildRichText(params RichTextEntry[] fragments) => BuildRichText(fragments.AsSpan());
|
||||
public static void RenderSeString(DalamudSeString seString, Vector2 position, ImFontPtr? font = null, ImDrawListPtr? drawList = null)
|
||||
{
|
||||
drawList ??= ImGui.GetWindowDrawList();
|
||||
|
||||
@@ -51,9 +90,36 @@ public static class SeStringUtils
|
||||
|
||||
ImGui.SetCursorScreenPos(position);
|
||||
ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams);
|
||||
|
||||
var textSize = ImGui.CalcTextSize(seString.TextValue);
|
||||
if (textSize.Y <= 0f)
|
||||
textSize.Y = ImGui.GetTextLineHeight();
|
||||
|
||||
ImGui.Dummy(new Vector2(0f, textSize.Y));
|
||||
}
|
||||
|
||||
public static Vector2 RenderSeStringWithHitbox(SeString seString, Vector2 position, ImFontPtr? font = null)
|
||||
public static void RenderSeStringWrapped(DalamudSeString seString, float wrapWidth, ImFontPtr? font = null, ImDrawListPtr? drawList = null)
|
||||
{
|
||||
drawList ??= ImGui.GetWindowDrawList();
|
||||
|
||||
var drawParams = new SeStringDrawParams
|
||||
{
|
||||
Font = font ?? ImGui.GetFont(),
|
||||
Color = ImGui.GetColorU32(ImGuiCol.Text),
|
||||
WrapWidth = wrapWidth,
|
||||
TargetDrawList = drawList
|
||||
};
|
||||
|
||||
ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams);
|
||||
|
||||
var calcWrapWidth = wrapWidth > 0f ? wrapWidth : -1f;
|
||||
var textSize = ImGui.CalcTextSize(seString.TextValue, wrapWidth: calcWrapWidth);
|
||||
if (textSize.Y <= 0f)
|
||||
textSize.Y = ImGui.GetTextLineHeight();
|
||||
|
||||
ImGui.Dummy(new Vector2(0f, textSize.Y));
|
||||
}
|
||||
public static Vector2 RenderSeStringWithHitbox(DalamudSeString seString, Vector2 position, ImFontPtr? font = null)
|
||||
{
|
||||
var drawList = ImGui.GetWindowDrawList();
|
||||
|
||||
@@ -99,6 +165,8 @@ public static class SeStringUtils
|
||||
|
||||
#region Internal Payloads
|
||||
|
||||
public readonly record struct RichTextEntry(string Text, Vector4? Color = null, bool Bold = false);
|
||||
|
||||
private abstract class AbstractColorPayload : Payload
|
||||
{
|
||||
protected byte Red { get; init; }
|
||||
|
||||
15
LightlessSync/Utils/UtilsEnum/LabelAlignment.cs
Normal file
15
LightlessSync/Utils/UtilsEnum/LabelAlignment.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LightlessSync.UtilsEnum.Enum
|
||||
{
|
||||
public enum LabelAlignment
|
||||
{
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Dalamud.Utility;
|
||||
using Dalamud.Utility;
|
||||
using K4os.Compression.LZ4.Legacy;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Dto.Files;
|
||||
@@ -8,6 +8,7 @@ using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.WebAPI.Files.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
@@ -19,7 +20,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
private readonly FileCompactor _fileCompactor;
|
||||
private readonly FileCacheManager _fileDbManager;
|
||||
private readonly FileTransferOrchestrator _orchestrator;
|
||||
private readonly List<ThrottledStream> _activeDownloadStreams;
|
||||
private readonly ConcurrentDictionary<ThrottledStream, byte> _activeDownloadStreams;
|
||||
|
||||
public FileDownloadManager(ILogger<FileDownloadManager> logger, LightlessMediator mediator,
|
||||
FileTransferOrchestrator orchestrator,
|
||||
@@ -29,14 +30,14 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
_orchestrator = orchestrator;
|
||||
_fileDbManager = fileCacheManager;
|
||||
_fileCompactor = fileCompactor;
|
||||
_activeDownloadStreams = [];
|
||||
_activeDownloadStreams = new();
|
||||
|
||||
Mediator.Subscribe<DownloadLimitChangedMessage>(this, (msg) =>
|
||||
{
|
||||
if (!_activeDownloadStreams.Any()) return;
|
||||
if (_activeDownloadStreams.IsEmpty) return;
|
||||
var newLimit = _orchestrator.DownloadLimitPerSlot();
|
||||
Logger.LogTrace("Setting new Download Speed Limit to {newLimit}", newLimit);
|
||||
foreach (var stream in _activeDownloadStreams)
|
||||
foreach (var stream in _activeDownloadStreams.Keys)
|
||||
{
|
||||
stream.BandwidthLimit = newLimit;
|
||||
}
|
||||
@@ -47,7 +48,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
|
||||
public List<FileTransfer> ForbiddenTransfers => _orchestrator.ForbiddenTransfers;
|
||||
|
||||
public bool IsDownloading => !CurrentDownloads.Any();
|
||||
public bool IsDownloading => CurrentDownloads.Any();
|
||||
|
||||
public static void MungeBuffer(Span<byte> buffer)
|
||||
{
|
||||
@@ -84,7 +85,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
ClearDownload();
|
||||
foreach (var stream in _activeDownloadStreams.ToList())
|
||||
foreach (var stream in _activeDownloadStreams.Keys.ToList())
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -95,6 +96,10 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
// do nothing
|
||||
//
|
||||
}
|
||||
finally
|
||||
{
|
||||
_activeDownloadStreams.TryRemove(stream, out _);
|
||||
}
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
@@ -142,7 +147,14 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
|
||||
await WaitForDownloadReady(fileTransfer, requestId, ct).ConfigureAwait(false);
|
||||
|
||||
_downloadStatus[downloadGroup].DownloadStatus = DownloadStatus.Downloading;
|
||||
if (_downloadStatus.TryGetValue(downloadGroup, out var downloadStatus))
|
||||
{
|
||||
downloadStatus.DownloadStatus = DownloadStatus.Downloading;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning("Download status missing for {group} when starting download", downloadGroup);
|
||||
}
|
||||
|
||||
const int maxRetries = 3;
|
||||
int retryCount = 0;
|
||||
@@ -204,7 +216,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
|
||||
stream = new(await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false), limit);
|
||||
|
||||
_activeDownloadStreams.Add(stream);
|
||||
_activeDownloadStreams.TryAdd(stream, 0);
|
||||
|
||||
while ((bytesRead = await stream.ReadAsync(buffer, ct).ConfigureAwait(false)) > 0)
|
||||
{
|
||||
@@ -245,7 +257,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
{
|
||||
if (stream != null)
|
||||
{
|
||||
_activeDownloadStreams.Remove(stream);
|
||||
_activeDownloadStreams.TryRemove(stream, out _);
|
||||
await stream.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -253,11 +265,28 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
|
||||
public async Task<List<DownloadFileTransfer>> InitiateDownloadList(GameObjectHandler gameObjectHandler, List<FileReplacementData> fileReplacement, CancellationToken ct)
|
||||
{
|
||||
Logger.LogDebug("Download start: {id}", gameObjectHandler.Name);
|
||||
var objectName = gameObjectHandler?.Name ?? "Unknown";
|
||||
Logger.LogDebug("Download start: {id}", objectName);
|
||||
|
||||
if (fileReplacement == null || fileReplacement.Count == 0)
|
||||
{
|
||||
Logger.LogDebug("{dlName}: No file replacements provided", objectName);
|
||||
CurrentDownloads = [];
|
||||
return CurrentDownloads;
|
||||
}
|
||||
|
||||
var hashes = fileReplacement.Where(f => f != null && !string.IsNullOrWhiteSpace(f.Hash)).Select(f => f.Hash).Distinct(StringComparer.Ordinal).ToList();
|
||||
|
||||
if (hashes.Count == 0)
|
||||
{
|
||||
Logger.LogDebug("{dlName}: No valid hashes to download", objectName);
|
||||
CurrentDownloads = [];
|
||||
return CurrentDownloads;
|
||||
}
|
||||
|
||||
List<DownloadFileDto> downloadFileInfoFromService =
|
||||
[
|
||||
.. await FilesGetSizes(fileReplacement.Select(f => f.Hash).Distinct(StringComparer.Ordinal).ToList(), ct).ConfigureAwait(false),
|
||||
.. await FilesGetSizes(hashes, ct).ConfigureAwait(false),
|
||||
];
|
||||
|
||||
Logger.LogDebug("Files with size 0 or less: {files}", string.Join(", ", downloadFileInfoFromService.Where(f => f.Size <= 0).Select(f => f.Hash)));
|
||||
@@ -315,15 +344,23 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
FileInfo fi = new(blockFile);
|
||||
try
|
||||
{
|
||||
_downloadStatus[fileGroup.Key].DownloadStatus = DownloadStatus.WaitingForSlot;
|
||||
if (!_downloadStatus.TryGetValue(fileGroup.Key, out var downloadStatus))
|
||||
{
|
||||
Logger.LogWarning("Download status missing for {group}, aborting", fileGroup.Key);
|
||||
return;
|
||||
}
|
||||
|
||||
downloadStatus.DownloadStatus = DownloadStatus.WaitingForSlot;
|
||||
await _orchestrator.WaitForDownloadSlotAsync(token).ConfigureAwait(false);
|
||||
_downloadStatus[fileGroup.Key].DownloadStatus = DownloadStatus.WaitingForQueue;
|
||||
downloadStatus.DownloadStatus = DownloadStatus.WaitingForQueue;
|
||||
Progress<long> progress = new((bytesDownloaded) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_downloadStatus.TryGetValue(fileGroup.Key, out FileDownloadStatus? value)) return;
|
||||
value.TransferredBytes += bytesDownloaded;
|
||||
if (_downloadStatus.TryGetValue(fileGroup.Key, out FileDownloadStatus? value))
|
||||
{
|
||||
value.TransferredBytes += bytesDownloaded;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -353,6 +390,12 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
status.TransferredFiles = 1;
|
||||
status.DownloadStatus = DownloadStatus.Decompressing;
|
||||
}
|
||||
if (!File.Exists(blockFile))
|
||||
{
|
||||
Logger.LogWarning("{dlName}: Block file missing before extraction, skipping", fi.Name);
|
||||
return;
|
||||
}
|
||||
|
||||
fileBlockStream = File.OpenRead(blockFile);
|
||||
while (fileBlockStream.Position < fileBlockStream.Length)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Dto.Files;
|
||||
using LightlessSync.API.Routes;
|
||||
using LightlessSync.FileCache;
|
||||
@@ -10,6 +10,8 @@ using LightlessSync.WebAPI.Files.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
|
||||
namespace LightlessSync.WebAPI.Files;
|
||||
|
||||
@@ -19,7 +21,9 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
||||
private readonly LightlessConfigService _lightlessConfigService;
|
||||
private readonly FileTransferOrchestrator _orchestrator;
|
||||
private readonly ServerConfigurationManager _serverManager;
|
||||
private readonly Dictionary<string, DateTime> _verifiedUploadedHashes = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, DateTime> _verifiedUploadedHashes = new(StringComparer.Ordinal);
|
||||
private readonly object _currentUploadsLock = new();
|
||||
private readonly Dictionary<string, FileTransfer> _currentUploadsByHash = new(StringComparer.Ordinal);
|
||||
private CancellationTokenSource? _uploadCancellationTokenSource = new();
|
||||
|
||||
public FileUploadManager(ILogger<FileUploadManager> logger, LightlessMediator mediator,
|
||||
@@ -40,17 +44,38 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
||||
}
|
||||
|
||||
public List<FileTransfer> CurrentUploads { get; } = [];
|
||||
public bool IsUploading => CurrentUploads.Count > 0;
|
||||
public bool IsUploading
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_currentUploadsLock)
|
||||
{
|
||||
return CurrentUploads.Count > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<FileTransfer> GetCurrentUploadsSnapshot()
|
||||
{
|
||||
lock (_currentUploadsLock)
|
||||
{
|
||||
return CurrentUploads.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public bool CancelUpload()
|
||||
{
|
||||
if (CurrentUploads.Any())
|
||||
if (IsUploading)
|
||||
{
|
||||
Logger.LogDebug("Cancelling current upload");
|
||||
_uploadCancellationTokenSource?.Cancel();
|
||||
_uploadCancellationTokenSource?.Dispose();
|
||||
_uploadCancellationTokenSource = null;
|
||||
CurrentUploads.Clear();
|
||||
lock (_currentUploadsLock)
|
||||
{
|
||||
CurrentUploads.Clear();
|
||||
_currentUploadsByHash.Clear();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -83,22 +108,44 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
||||
return [.. filesToUpload.Where(f => f.IsForbidden).Select(f => f.Hash)];
|
||||
}
|
||||
|
||||
Task uploadTask = Task.CompletedTask;
|
||||
var cancellationToken = ct ?? CancellationToken.None;
|
||||
var parallelUploads = Math.Clamp(_lightlessConfigService.Current.ParallelUploads, 1, 8);
|
||||
using SemaphoreSlim uploadSlots = new(parallelUploads, parallelUploads);
|
||||
List<Task> uploadTasks = new();
|
||||
|
||||
int i = 1;
|
||||
foreach (var file in filesToUpload)
|
||||
{
|
||||
progress.Report($"Uploading file {i++}/{filesToUpload.Count}. Please wait until the upload is completed.");
|
||||
Logger.LogDebug("[{hash}] Compressing", file);
|
||||
var data = await _fileDbManager.GetCompressedFileData(file.Hash, ct ?? CancellationToken.None).ConfigureAwait(false);
|
||||
Logger.LogDebug("[{hash}] Starting upload for {filePath}", data.Item1, _fileDbManager.GetFileCacheByHash(data.Item1)!.ResolvedFilepath);
|
||||
await uploadTask.ConfigureAwait(false);
|
||||
uploadTask = UploadFile(data.Item2, file.Hash, postProgress: false, ct ?? CancellationToken.None);
|
||||
(ct ?? CancellationToken.None).ThrowIfCancellationRequested();
|
||||
uploadTasks.Add(UploadSingleFileAsync(file, uploadSlots, cancellationToken));
|
||||
}
|
||||
|
||||
await uploadTask.ConfigureAwait(false);
|
||||
await Task.WhenAll(uploadTasks).ConfigureAwait(false);
|
||||
|
||||
return [];
|
||||
|
||||
async Task UploadSingleFileAsync(UploadFileDto fileDto, SemaphoreSlim gate, CancellationToken token)
|
||||
{
|
||||
await gate.WaitAsync(token).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
Logger.LogDebug("[{hash}] Compressing", fileDto.Hash);
|
||||
var data = await _fileDbManager.GetCompressedFileData(fileDto.Hash, token).ConfigureAwait(false);
|
||||
|
||||
var cacheEntry = _fileDbManager.GetFileCacheByHash(data.Item1);
|
||||
if (cacheEntry != null)
|
||||
{
|
||||
Logger.LogDebug("[{hash}] Starting upload for {filePath}", data.Item1, cacheEntry.ResolvedFilepath);
|
||||
}
|
||||
|
||||
await UploadFile(data.Item2, fileDto.Hash, postProgress: false, token).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
gate.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<CharacterData> UploadFiles(CharacterData data, List<UserData> visiblePlayers)
|
||||
@@ -167,7 +214,11 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
||||
_uploadCancellationTokenSource?.Cancel();
|
||||
_uploadCancellationTokenSource?.Dispose();
|
||||
_uploadCancellationTokenSource = null;
|
||||
CurrentUploads.Clear();
|
||||
lock (_currentUploadsLock)
|
||||
{
|
||||
CurrentUploads.Clear();
|
||||
_currentUploadsByHash.Clear();
|
||||
}
|
||||
_verifiedUploadedHashes.Clear();
|
||||
}
|
||||
|
||||
@@ -211,7 +262,17 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
||||
{
|
||||
try
|
||||
{
|
||||
CurrentUploads.Single(f => string.Equals(f.Hash, fileHash, StringComparison.Ordinal)).Transferred = prog.Uploaded;
|
||||
lock (_currentUploadsLock)
|
||||
{
|
||||
if (_currentUploadsByHash.TryGetValue(fileHash, out var transfer))
|
||||
{
|
||||
transfer.Transferred = prog.Uploaded;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogDebug("[{hash}] Could not find upload transfer during progress update", fileHash);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -240,10 +301,16 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
||||
{
|
||||
try
|
||||
{
|
||||
CurrentUploads.Add(new UploadFileTransfer(file)
|
||||
var uploadTransfer = new UploadFileTransfer(file)
|
||||
{
|
||||
Total = new FileInfo(_fileDbManager.GetFileCacheByHash(file.Hash)!.ResolvedFilepath).Length,
|
||||
});
|
||||
};
|
||||
|
||||
lock (_currentUploadsLock)
|
||||
{
|
||||
CurrentUploads.Add(uploadTransfer);
|
||||
_currentUploadsByHash[file.Hash] = uploadTransfer;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -264,33 +331,75 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
||||
_verifiedUploadedHashes[file.Hash] = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
var totalSize = CurrentUploads.Sum(c => c.Total);
|
||||
long totalSize;
|
||||
List<FileTransfer> pendingUploads;
|
||||
lock (_currentUploadsLock)
|
||||
{
|
||||
totalSize = CurrentUploads.Sum(c => c.Total);
|
||||
pendingUploads = CurrentUploads.Where(f => f.CanBeTransferred && !f.IsTransferred).ToList();
|
||||
}
|
||||
|
||||
var parallelUploads = Math.Clamp(_lightlessConfigService.Current.ParallelUploads, 1, 8);
|
||||
using SemaphoreSlim uploadSlots = new(parallelUploads, parallelUploads);
|
||||
Logger.LogDebug("Compressing and uploading files");
|
||||
Task uploadTask = Task.CompletedTask;
|
||||
foreach (var file in CurrentUploads.Where(f => f.CanBeTransferred && !f.IsTransferred).ToList())
|
||||
List<Task> uploadTasks = new();
|
||||
|
||||
foreach (var transfer in pendingUploads)
|
||||
{
|
||||
Logger.LogDebug("[{hash}] Compressing", file);
|
||||
var data = await _fileDbManager.GetCompressedFileData(file.Hash, uploadToken).ConfigureAwait(false);
|
||||
CurrentUploads.Single(e => string.Equals(e.Hash, data.Item1, StringComparison.Ordinal)).Total = data.Item2.Length;
|
||||
Logger.LogDebug("[{hash}] Starting upload for {filePath}", data.Item1, _fileDbManager.GetFileCacheByHash(data.Item1)!.ResolvedFilepath);
|
||||
await uploadTask.ConfigureAwait(false);
|
||||
uploadTask = UploadFile(data.Item2, file.Hash, true, uploadToken);
|
||||
uploadToken.ThrowIfCancellationRequested();
|
||||
uploadTasks.Add(UploadPendingFileAsync(transfer, uploadSlots, uploadToken));
|
||||
}
|
||||
|
||||
if (CurrentUploads.Any())
|
||||
{
|
||||
await uploadTask.ConfigureAwait(false);
|
||||
await Task.WhenAll(uploadTasks).ConfigureAwait(false);
|
||||
|
||||
var compressedSize = CurrentUploads.Sum(c => c.Total);
|
||||
Logger.LogDebug("Upload complete, compressed {size} to {compressed}", UiSharedService.ByteToString(totalSize), UiSharedService.ByteToString(compressedSize));
|
||||
long compressedSize;
|
||||
HashSet<string> uploadedHashes;
|
||||
lock (_currentUploadsLock)
|
||||
{
|
||||
compressedSize = CurrentUploads.Sum(c => c.Total);
|
||||
uploadedHashes = CurrentUploads.Select(u => u.Hash).ToHashSet(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
foreach (var file in unverifiedUploadHashes.Where(c => !CurrentUploads.Exists(u => string.Equals(u.Hash, c, StringComparison.Ordinal))))
|
||||
Logger.LogDebug("Upload complete, compressed {size} to {compressed}", UiSharedService.ByteToString(totalSize), UiSharedService.ByteToString(compressedSize));
|
||||
|
||||
foreach (var file in unverifiedUploadHashes.Where(c => !uploadedHashes.Contains(c)))
|
||||
{
|
||||
_verifiedUploadedHashes[file] = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
CurrentUploads.Clear();
|
||||
lock (_currentUploadsLock)
|
||||
{
|
||||
CurrentUploads.Clear();
|
||||
_currentUploadsByHash.Clear();
|
||||
}
|
||||
|
||||
async Task UploadPendingFileAsync(FileTransfer transfer, SemaphoreSlim gate, CancellationToken token)
|
||||
{
|
||||
await gate.WaitAsync(token).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
Logger.LogDebug("[{hash}] Compressing", transfer.Hash);
|
||||
var data = await _fileDbManager.GetCompressedFileData(transfer.Hash, token).ConfigureAwait(false);
|
||||
lock (_currentUploadsLock)
|
||||
{
|
||||
if (_currentUploadsByHash.TryGetValue(data.Item1, out var trackedUpload))
|
||||
{
|
||||
trackedUpload.Total = data.Item2.Length;
|
||||
}
|
||||
}
|
||||
|
||||
var cacheEntry = _fileDbManager.GetFileCacheByHash(data.Item1);
|
||||
if (cacheEntry != null)
|
||||
{
|
||||
Logger.LogDebug("[{hash}] Starting upload for {filePath}", data.Item1, cacheEntry.ResolvedFilepath);
|
||||
}
|
||||
|
||||
await UploadFile(data.Item2, transfer.Hash, true, token).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
gate.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,16 +35,16 @@ public partial class ApiController
|
||||
await _lightlessHub!.SendAsync(nameof(UserAddPair), user).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task TryPairWithContentId(string otherCid, string myCid)
|
||||
public async Task TryPairWithContentId(string otherCid)
|
||||
{
|
||||
if (!IsConnected) return;
|
||||
await _lightlessHub!.SendAsync(nameof(TryPairWithContentId), otherCid, myCid).ConfigureAwait(false);
|
||||
await _lightlessHub!.SendAsync(nameof(TryPairWithContentId), otherCid).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task SetBroadcastStatus(string hashedCid, bool enabled, GroupBroadcastRequestDto? groupDto = null)
|
||||
public async Task SetBroadcastStatus(bool enabled, GroupBroadcastRequestDto? groupDto = null)
|
||||
{
|
||||
CheckConnection();
|
||||
await _lightlessHub!.InvokeAsync(nameof(SetBroadcastStatus), hashedCid, enabled, groupDto).ConfigureAwait(false);
|
||||
await _lightlessHub!.InvokeAsync(nameof(SetBroadcastStatus), enabled, groupDto).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<BroadcastStatusInfoDto?> IsUserBroadcasting(string hashedCid)
|
||||
@@ -59,10 +59,10 @@ public partial class ApiController
|
||||
return await _lightlessHub!.InvokeAsync<BroadcastStatusBatchDto>(nameof(AreUsersBroadcasting), hashedCids).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<TimeSpan?> GetBroadcastTtl(string hashedCid)
|
||||
public async Task<TimeSpan?> GetBroadcastTtl()
|
||||
{
|
||||
CheckConnection();
|
||||
return await _lightlessHub!.InvokeAsync<TimeSpan?>(nameof(GetBroadcastTtl), hashedCid).ConfigureAwait(false);
|
||||
return await _lightlessHub!.InvokeAsync<TimeSpan?>(nameof(GetBroadcastTtl)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task UserDelete()
|
||||
@@ -134,6 +134,12 @@ public partial class ApiController
|
||||
await _lightlessHub!.InvokeAsync(nameof(UserSetProfile), userDescription).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task UserUpdateVanityColors(UserVanityColorsDto dto)
|
||||
{
|
||||
if (!IsConnected) return;
|
||||
await _lightlessHub!.InvokeAsync(nameof(UserUpdateVanityColors), dto).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task UserUpdateDefaultPermissions(DefaultPermissionsDto defaultPermissionsDto)
|
||||
{
|
||||
CheckConnection();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.API.Dto;
|
||||
using LightlessSync.API.Dto.CharaData;
|
||||
@@ -6,6 +6,7 @@ using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Utils;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -104,10 +105,27 @@ public partial class ApiController
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
public Task Client_ReceiveBroadcastPairRequest(UserPairNotificationDto dto)
|
||||
{
|
||||
if (dto == null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
var request = _pairRequestService.RegisterIncomingRequest(dto.myHashedCid, dto.message ?? string.Empty);
|
||||
var senderName = string.IsNullOrEmpty(request.DisplayName) ? "Unknown User" : request.DisplayName;
|
||||
|
||||
_lightlessNotificationService.ShowPairRequestNotification(
|
||||
senderName,
|
||||
request.HashedCid,
|
||||
onAccept: () => _pairRequestService.AcceptPairRequest(request.HashedCid, senderName),
|
||||
onDecline: () => _pairRequestService.DeclinePairRequest(request.HashedCid));
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_UpdateSystemInfo(SystemInfoDto systemInfo)
|
||||
{
|
||||
SystemInfoDto = systemInfo;
|
||||
//Mediator.Publish(new UpdateSystemInfoMessage(systemInfo));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -277,12 +295,25 @@ public partial class ApiController
|
||||
_lightlessHub!.On(nameof(Client_GroupSendInfo), act);
|
||||
}
|
||||
|
||||
|
||||
public void OnGroupUpdateProfile(Action<GroupProfileDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_lightlessHub!.On(nameof(Client_GroupSendProfile), act);
|
||||
}
|
||||
|
||||
public void OnReceiveServerMessage(Action<MessageSeverity, string> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_lightlessHub!.On(nameof(Client_ReceiveServerMessage), act);
|
||||
}
|
||||
|
||||
public void OnReceiveBroadcastPairRequest(Action<UserPairNotificationDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
_lightlessHub!.On(nameof(Client_ReceiveBroadcastPairRequest), act);
|
||||
}
|
||||
|
||||
public void OnUpdateSystemInfo(Action<SystemInfoDto> act)
|
||||
{
|
||||
if (_initialized) return;
|
||||
|
||||
@@ -45,6 +45,11 @@ public partial class ApiController
|
||||
CheckConnection();
|
||||
await _lightlessHub!.SendAsync(nameof(GroupClear), group).ConfigureAwait(false);
|
||||
}
|
||||
public async Task GroupClearFinder(GroupDto group)
|
||||
{
|
||||
CheckConnection();
|
||||
await _lightlessHub!.SendAsync(nameof(GroupClearFinder), group).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<GroupJoinDto> GroupCreate()
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Dalamud.Utility;
|
||||
using Dalamud.Utility;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.API.Dto;
|
||||
@@ -28,9 +28,11 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly HubFactory _hubFactory;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly PairRequestService _pairRequestService;
|
||||
private readonly ServerConfigurationManager _serverManager;
|
||||
private readonly TokenProvider _tokenProvider;
|
||||
private readonly LightlessConfigService _lightlessConfigService;
|
||||
private readonly NotificationService _lightlessNotificationService;
|
||||
private CancellationTokenSource _connectionCancellationTokenSource;
|
||||
private ConnectionDto? _connectionDto;
|
||||
private bool _doNotNotifyOnNextInfo = false;
|
||||
@@ -42,15 +44,17 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
||||
private CensusUpdateMessage? _lastCensus;
|
||||
|
||||
public ApiController(ILogger<ApiController> logger, HubFactory hubFactory, DalamudUtilService dalamudUtil,
|
||||
PairManager pairManager, ServerConfigurationManager serverManager, LightlessMediator mediator,
|
||||
TokenProvider tokenProvider, LightlessConfigService lightlessConfigService) : base(logger, mediator)
|
||||
PairManager pairManager, PairRequestService pairRequestService, ServerConfigurationManager serverManager, LightlessMediator mediator,
|
||||
TokenProvider tokenProvider, LightlessConfigService lightlessConfigService, NotificationService lightlessNotificationService) : base(logger, mediator)
|
||||
{
|
||||
_hubFactory = hubFactory;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_pairManager = pairManager;
|
||||
_pairRequestService = pairRequestService;
|
||||
_serverManager = serverManager;
|
||||
_tokenProvider = tokenProvider;
|
||||
_lightlessConfigService = lightlessConfigService;
|
||||
_lightlessNotificationService = lightlessNotificationService;
|
||||
_connectionCancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
Mediator.Subscribe<DalamudLoginMessage>(this, (_) => DalamudUtilOnLogIn());
|
||||
@@ -77,6 +81,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
||||
public DefaultPermissionsDto? DefaultPermissions => _connectionDto?.DefaultPreferredPermissions ?? null;
|
||||
public string DisplayName => _connectionDto?.User.AliasOrUID ?? string.Empty;
|
||||
|
||||
public bool HasVanity => _connectionDto?.HasVanity ?? false;
|
||||
public string TextColorHex => _connectionDto?.TextColorHex ?? string.Empty;
|
||||
public string TextGlowColorHex => _connectionDto?.TextGlowColorHex ?? string.Empty;
|
||||
|
||||
public bool IsConnected => ServerState == ServerState.Connected;
|
||||
|
||||
public bool IsCurrentVersion => (Assembly.GetExecutingAssembly().GetName().Version ?? new Version(0, 0, 0, 0)) >= (_connectionDto?.CurrentClientVersion ?? new Version(0, 0, 0, 0));
|
||||
@@ -424,6 +432,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
||||
Logger.LogDebug("Initializing data");
|
||||
OnDownloadReady((guid) => _ = Client_DownloadReady(guid));
|
||||
OnReceiveServerMessage((sev, msg) => _ = Client_ReceiveServerMessage(sev, msg));
|
||||
OnReceiveBroadcastPairRequest(dto => _ = Client_ReceiveBroadcastPairRequest(dto));
|
||||
OnUpdateSystemInfo((dto) => _ = Client_UpdateSystemInfo(dto));
|
||||
|
||||
OnUserSendOffline((dto) => _ = Client_UserSendOffline(dto));
|
||||
@@ -445,6 +454,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
||||
OnGroupPairLeft((dto) => _ = Client_GroupPairLeft(dto));
|
||||
OnGroupSendFullInfo((dto) => _ = Client_GroupSendFullInfo(dto));
|
||||
OnGroupSendInfo((dto) => _ = Client_GroupSendInfo(dto));
|
||||
OnGroupUpdateProfile((dto) => _ = Client_GroupSendProfile(dto));
|
||||
OnGroupChangeUserPairPermissions((dto) => _ = Client_GroupChangeUserPairPermissions(dto));
|
||||
|
||||
OnGposeLobbyJoin((dto) => _ = Client_GposeLobbyJoin(dto));
|
||||
|
||||
Submodule PenumbraAPI updated: dd14131793...648b6fc2ce
Reference in New Issue
Block a user