Compare commits

...

68 Commits

Author SHA1 Message Date
choco
b9030549a7 pair request cleanup with refactoring 2025-10-16 14:08:37 +02:00
choco
10336a829a added re-broadcast to the notifications with action buttons, added some regular notifcations to broadcast service 2025-10-16 12:28:34 +02:00
77ff8ae372 Merge pull request 'notification-changes' (#65) from notification-changes into 1.12.3
Reviewed-on: #65
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-10-14 22:43:10 +02:00
choco
cf27a67296 optional action button toggle for pair request (default on) 2025-10-14 15:07:46 +02:00
choco
d6a4595bb8 old debug line removals, better alignment performance notifs 2025-10-14 14:54:30 +02:00
choco
f202818b55 performance notifcation addition, with some regular bugfixes regarding the flexing of the notifications 2025-10-14 11:46:14 +02:00
choco
3f2e4d6640 small service cleanup 2025-10-14 09:36:10 +02:00
choco
90b483e4ea Merge remote-tracking branch 'origin/1.12.3' into notification-changes 2025-10-13 22:24:50 +02:00
choco
bcb524df52 text auto flexing on dismiss removed 2025-10-13 00:13:21 +02:00
b64bb66119 Merge pull request 'notification-changes' (#62) from notification-changes into 1.12.3
Reviewed-on: #62
2025-10-12 23:33:42 +02:00
choco
118edb9dea notif overlay flex with 1 sec delay removal 2025-10-12 23:11:18 +02:00
choco
6c0d00dc39 update intervall prevent spam better performance 2025-10-12 21:07:32 +02:00
defnotken
dae8127ac8 Merge branch '1.12.3' of https://git.lightless-sync.org/Lightless-Sync/LightlessClient into 1.12.3 2025-10-12 12:11:18 -05:00
defnotken
0635caab65 Safety checks for NullDrawObject 2025-10-12 12:09:06 -05:00
1530ac3911 Merge pull request 'notification-changes' (#59) from notification-changes into 1.12.3
Reviewed-on: #59
2025-10-12 18:08:52 +02:00
3f80467180 Merge branch '1.12.3' into notification-changes 2025-10-12 18:08:43 +02:00
defnotken
4b4e587a89 1.12.3 initial 2025-10-12 11:07:24 -05:00
choco
02c3846031 column rename for better UX c: 2025-10-12 18:02:53 +02:00
choco
a8a01b3034 added more customization to the notifs, settings improvemnts, left and right notifs, animations for sliding in and out 2025-10-12 18:00:49 +02:00
50a5046c96 1.12.2
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 51s
Reviewed-on: #52
2025-10-12 15:33:02 +02:00
azyges
c0b8e15380 wrapped the entire plugin draw pass in a single theme scope, prevent leaks to dalamud ui 2025-10-12 21:48:51 +09:00
e6735be594 Merge pull request 'pair-notifs-ui' (#58) from pair-notifs-ui into 1.12.2
Reviewed-on: #58
2025-10-12 00:05:53 +02:00
choco
fe419336d7 math clamping sliders for notifcation settings, pair notifs disappear now when accepted with other methods 2025-10-12 00:04:10 +02:00
defnotken
ffbeeba929 more null checks for nameplates. 2025-10-11 16:39:37 -05:00
choco
a7475a7007 sounds to default off removed old notifcations panel with unused pair panel 2025-10-11 23:08:29 +02:00
choco
3936cbd439 can only be run on Framework fix 2025-10-11 22:54:04 +02:00
choco
ba16963b66 forgot dependency injection error fix 2025-10-11 22:25:54 +02:00
6467a3e73b Merge pull request 'pair-notifs-ui' (#57) from pair-notifs-ui into 1.12.2
Reviewed-on: #57
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-10-11 22:19:21 +02:00
choco
bb779904f7 removed temp accept/decline logic from the api layer 2025-10-11 21:52:14 +02:00
choco
59d0e8ee37 Merge branch '1.12.2' into pair-notifs-ui
# Conflicts:
#	LightlessSync/UI/SettingsUi.cs
2025-10-11 21:42:14 +02:00
choco
a441bbfcc8 notifcation refactor for better readability 2025-10-11 21:24:39 +02:00
choco
c545ccea52 X offset 2025-10-11 20:19:24 +02:00
azyges
c32d9cadff fix leak 2025-10-11 10:15:51 +09:00
azyges
d7c9df54cb update cache 2025-10-11 08:20:50 +09:00
37ec0961d9 Merge pull request 'lightfinder-partial-rework' (#56) from lightfinder-partial-rework into 1.12.2
Reviewed-on: #56
2025-10-11 01:04:27 +02:00
9736c5090d Merge branch '1.12.2' into lightfinder-partial-rework 2025-10-11 01:04:12 +02:00
defnotken
4f3ab604db update lightlessapi pointer 2025-10-10 18:03:57 -05:00
azyges
6a0f8c507c add seperate colors for labels and update color inputs 2025-10-11 07:52:52 +09:00
choco
e13fde3d43 improved settings with sounds bug fix 2025-10-11 00:46:18 +02:00
7b806ab660 Merge pull request 'Nameplate Fix + Text Handling' (#55) from nameplate-fix into 1.12.2
Reviewed-on: #55
Reviewed-by: Essie <azyges@noreply.git.lightless-sync.org>
2025-10-10 23:35:27 +02:00
defnotken
387e5ad515 oop 2025-10-10 16:27:40 -05:00
defnotken
70c296a16b Nameplate Fix + Text Handling 2025-10-10 16:26:36 -05:00
azyges
2a9b5812ed add theme override customizations 2025-10-11 03:29:44 +09:00
azyges
9b04976aa6 add info options for server bar, direct settings button in lightfinder window and fix color swaps 2025-10-11 00:48:19 +09:00
CakeAndBanana
144ac166fb Changed Discord URL 2025-10-10 15:50:22 +02:00
defnotken
b06ffb3341 update submodule 2025-10-09 18:06:31 -05:00
e9461efe11 Merge pull request 'Added option to show hidden plates' (#54) from hidden-plates-finder into 1.12.2
Reviewed-on: #54
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-10-10 01:05:34 +02:00
1f1afdec24 Merge branch '1.12.2' into hidden-plates-finder 2025-10-10 01:05:26 +02:00
d428a436e7 Merge pull request 'Changed /light lightfinder to /light finder' (#53) from command-change-finder into 1.12.2
Reviewed-on: #53
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-10-10 01:05:18 +02:00
CakeAndBanana
ad29fa7b69 Fixed label 2025-10-09 23:31:45 +02:00
CakeAndBanana
23c56505ac Added option to show 2025-10-09 23:31:35 +02:00
CakeAndBanana
58850f4530 Changed /light lightfinder to /light finder 2025-10-09 23:01:50 +02:00
choco
f5339dc1d2 notif offset placement, default slider yoinked from abel 2025-10-09 22:53:01 +02:00
choco
85ecea6391 settings styling and sound disabled not working bugfix 2025-10-09 20:21:01 +02:00
choco
cd817487e4 scoped service crash fix 2025-10-09 19:18:27 +02:00
choco
f50b622f0a service cleanup 2025-10-09 15:50:59 +02:00
choco
d295f3e22d pair/downloads notif changes + more settings options 2025-10-09 13:56:40 +02:00
choco
0dfa667ed3 removed fallback logic in NotificationService and some settings cleanup 2025-10-09 11:31:35 +02:00
choco
2b118df892 notifications refactor with duplication bugfix 2025-10-09 11:13:47 +02:00
azyges
f01229a97f rework lightfinder for new api 2025-10-09 07:33:49 +09:00
choco
3fdc9dd958 Merge remote-tracking branch 'origin/1.12.2' into pair-notifs-ui 2025-10-08 23:49:07 +02:00
CakeAndBanana
86107acf12 version bumped 2025-10-08 23:39:07 +02:00
choco
27e7fb7ed9 more to notification system with new settings tab 2025-10-08 23:20:58 +02:00
choco
17f4ddad89 Merge tag '1.12.1' into pair-notifs-ui 2025-10-08 20:31:33 +02:00
a29e155cec 1.12.1: QoL Lightfinder
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 45s
Reviewed-on: #43
2025-10-08 19:57:23 +02:00
choco
9b6d00570e implemened game sound effects for notifs 2025-10-06 21:55:45 +02:00
choco
83e4555e4b notifications improvement, working pairs incoming request feature and working user logging in notif 2025-10-06 20:25:47 +02:00
choco
090b81c989 added notification system for file downloads and pair requests 2025-10-06 16:14:34 +02:00
39 changed files with 5091 additions and 913 deletions

View File

@@ -16,6 +16,8 @@ public sealed class FileCacheManager : IHostedService
public const string CachePrefix = "{cache}"; public const string CachePrefix = "{cache}";
public const string CsvSplit = "|"; public const string CsvSplit = "|";
public const string PenumbraPrefix = "{penumbra}"; public const string PenumbraPrefix = "{penumbra}";
private const int FileCacheVersion = 1;
private const string FileCacheVersionHeaderPrefix = "#lightless-file-cache-version:";
private readonly LightlessConfigService _configService; private readonly LightlessConfigService _configService;
private readonly LightlessMediator _lightlessMediator; private readonly LightlessMediator _lightlessMediator;
private readonly string _csvPath; private readonly string _csvPath;
@@ -54,6 +56,62 @@ public sealed class FileCacheManager : IHostedService
return NormalizeSeparators(prefixedPath).ToLowerInvariant(); 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) private string NormalizeToPrefixedPath(string path)
{ {
if (string.IsNullOrEmpty(path)) return string.Empty; if (string.IsNullOrEmpty(path)) return string.Empty;
@@ -66,27 +124,25 @@ public sealed class FileCacheManager : IHostedService
return NormalizePrefixedPathKey(normalized); return NormalizePrefixedPathKey(normalized);
} }
var penumbraDir = _ipcManager.Penumbra.ModDirectory; string? chosenPrefixed = null;
if (!string.IsNullOrEmpty(penumbraDir)) var chosenLength = -1;
if (TryBuildPrefixedPath(normalized, _ipcManager.Penumbra.ModDirectory, PenumbraPrefix, out var penumbraPrefixed, out var penumbraMatch))
{ {
var normalizedPenumbra = NormalizeSeparators(penumbraDir); chosenPrefixed = penumbraPrefixed;
var replacement = normalizedPenumbra.EndsWith("\\", StringComparison.Ordinal) chosenLength = penumbraMatch;
? PenumbraPrefix + "\\"
: PenumbraPrefix;
normalized = normalized.Replace(normalizedPenumbra, replacement, StringComparison.OrdinalIgnoreCase);
} }
var cacheFolder = _configService.Current.CacheFolder; if (TryBuildPrefixedPath(normalized, _configService.Current.CacheFolder, CachePrefix, out var cachePrefixed, out var cacheMatch))
if (!string.IsNullOrEmpty(cacheFolder))
{ {
var normalizedCache = NormalizeSeparators(cacheFolder); if (cacheMatch > chosenLength)
var replacement = normalizedCache.EndsWith("\\", StringComparison.Ordinal) {
? CachePrefix + "\\" chosenPrefixed = cachePrefixed;
: CachePrefix; chosenLength = cacheMatch;
normalized = normalized.Replace(normalizedCache, replacement, StringComparison.OrdinalIgnoreCase); }
} }
return NormalizePrefixedPathKey(normalized); return NormalizePrefixedPathKey(chosenPrefixed ?? normalized);
} }
public FileCacheEntity? CreateCacheEntry(string path) public FileCacheEntity? CreateCacheEntry(string path)
@@ -94,7 +150,9 @@ public sealed class FileCacheManager : IHostedService
FileInfo fi = new(path); FileInfo fi = new(path);
if (!fi.Exists) return null; if (!fi.Exists) return null;
_logger.LogTrace("Creating cache entry for {path}", path); _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) public FileCacheEntity? CreateFileEntry(string path)
@@ -102,14 +160,18 @@ public sealed class FileCacheManager : IHostedService
FileInfo fi = new(path); FileInfo fi = new(path);
if (!fi.Exists) return null; if (!fi.Exists) return null;
_logger.LogTrace("Creating file entry for {path}", path); _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) private FileCacheEntity? CreateFileEntity(string directory, string prefix, FileInfo fi)
{ {
var fullName = fi.FullName.ToLowerInvariant(); if (!TryBuildPrefixedPath(fi.FullName, directory, prefix, out var prefixedPath, out _))
if (!fullName.Contains(directory, StringComparison.Ordinal)) return null; {
string prefixedPath = fullName.Replace(directory, prefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal); return null;
}
return CreateFileCacheEntity(fi, prefixedPath); return CreateFileCacheEntity(fi, prefixedPath);
} }
@@ -367,6 +429,7 @@ public sealed class FileCacheManager : IHostedService
lock (_fileWriteLock) lock (_fileWriteLock)
{ {
StringBuilder sb = new(); StringBuilder sb = new();
sb.AppendLine(BuildVersionHeader());
foreach (var entry in _fileCaches.Values.SelectMany(k => k.Values).OrderBy(f => f.PrefixedFilePath, StringComparer.OrdinalIgnoreCase)) foreach (var entry in _fileCaches.Values.SelectMany(k => k.Values).OrderBy(f => f.PrefixedFilePath, StringComparer.OrdinalIgnoreCase))
{ {
sb.AppendLine(entry.CsvEntry); sb.AppendLine(entry.CsvEntry);
@@ -389,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) internal FileCacheEntity MigrateFileHashToExtension(FileCacheEntity fileCache, string ext)
{ {
try try
@@ -427,7 +537,15 @@ public sealed class FileCacheManager : IHostedService
AddHashedFile(entity); AddHashedFile(entity);
lock (_fileWriteLock) 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); var result = GetFileCacheByPath(fileInfo.FullName);
_logger.LogTrace("Creating cache entity for {name} success: {success}", fileInfo.FullName, (result != null)); _logger.LogTrace("Creating cache entity for {name} success: {success}", fileInfo.FullName, (result != null));
@@ -546,49 +664,111 @@ public sealed class FileCacheManager : IHostedService
_logger.LogWarning("Could not load entries from {path}, continuing with empty file cache", _csvPath); _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); if (entries.Length > 0)
foreach (var entry in entries)
{ {
var splittedEntry = entry.Split(CsvSplit, StringSplitOptions.None); var headerLine = entries[0];
try var hasHeader = !string.IsNullOrEmpty(headerLine) &&
headerLine.StartsWith(FileCacheVersionHeaderPrefix, StringComparison.OrdinalIgnoreCase);
if (hasHeader)
{ {
var hash = splittedEntry[0]; if (!TryParseVersionHeader(headerLine, out var parsedVersion))
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); _logger.LogWarning("Failed to parse file cache version header \"{header}\". Backing up existing cache.", headerLine);
continue; BackupUnsupportedCache("invalid-version");
parseEntries = false;
rewriteRequired = true;
entries = Array.Empty<string>();
} }
else if (parsedVersion != FileCacheVersion)
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)) _logger.LogWarning("Unsupported file cache version {version} detected (expected {expected}). Backing up existing cache.", parsedVersion, FileCacheVersion);
{ BackupUnsupportedCache($"v{parsedVersion}");
size = result; parseEntries = false;
} rewriteRequired = true;
if (long.TryParse(splittedEntry[4], CultureInfo.InvariantCulture, out long resultCompressed)) entries = Array.Empty<string>();
{ }
compressed = resultCompressed; 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(); WriteOutFullCsv();
} }

View File

@@ -0,0 +1,7 @@
namespace LightlessSync.LightlessConfiguration.Configurations;
public enum LightfinderDtrDisplayMode
{
NearbyBroadcasts = 0,
PendingPairRequests = 1,
}

View File

@@ -22,6 +22,13 @@ public class LightlessConfig : ILightlessConfiguration
public DtrEntry.Colors DtrColorsDefault { get; set; } = default; public DtrEntry.Colors DtrColorsDefault { get; set; } = default;
public DtrEntry.Colors DtrColorsNotConnected { get; set; } = new(Glow: 0x0428FFu); public DtrEntry.Colors DtrColorsNotConnected { get; set; } = new(Glow: 0x0428FFu);
public DtrEntry.Colors DtrColorsPairsInRange { get; set; } = new(Glow: 0xFFBA47u); public DtrEntry.Colors DtrColorsPairsInRange { get; set; } = new(Glow: 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 UseLightlessRedesign { get; set; } = true;
public bool EnableRightClickMenus { get; set; } = true; public bool EnableRightClickMenus { get; set; } = true;
public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both; public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both;
@@ -56,6 +63,7 @@ public class LightlessConfig : ILightlessConfiguration
public bool ShowOnlineNotificationsOnlyForNamedPairs { get; set; } = false; public bool ShowOnlineNotificationsOnlyForNamedPairs { get; set; } = false;
public bool ShowTransferBars { get; set; } = true; public bool ShowTransferBars { get; set; } = true;
public bool ShowTransferWindow { get; set; } = false; public bool ShowTransferWindow { get; set; } = false;
public bool UseNotificationsForDownloads { get; set; } = true;
public bool ShowUploading { get; set; } = true; public bool ShowUploading { get; set; } = true;
public bool ShowUploadingBigText { get; set; } = true; public bool ShowUploadingBigText { get; set; } = true;
public bool ShowVisibleUsersSeparately { get; set; } = true; public bool ShowVisibleUsersSeparately { get; set; } = true;
@@ -69,17 +77,68 @@ public class LightlessConfig : ILightlessConfiguration
public bool AutoPopulateEmptyNotesFromCharaName { get; set; } = false; public bool AutoPopulateEmptyNotesFromCharaName { get; set; } = false;
public int Version { get; set; } = 1; public int Version { get; set; } = 1;
public NotificationLocation WarningNotification { get; set; } = NotificationLocation.Both; 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;
public NotificationLocation LightlessPerformanceNotification { get; set; } = NotificationLocation.LightlessUi;
// 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 NotificationCorner NotificationCorner { get; set; } = NotificationCorner.Right;
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 NotificationSlideSpeed { 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 int PerformanceNotificationDurationSeconds { get; set; } = 20;
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 PerformanceSoundId { get; set; } = 16; // Se15
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 DisablePerformanceSound { get; set; } = true;
public bool ShowPerformanceNotificationActions { get; set; } = true;
public bool ShowPairRequestNotificationActions { get; set; } = true;
public bool UseFocusTarget { get; set; } = false; public bool UseFocusTarget { get; set; } = false;
public bool overrideFriendColor { get; set; } = false; public bool overrideFriendColor { get; set; } = false;
public bool overridePartyColor { get; set; } = false; public bool overridePartyColor { get; set; } = false;
public bool overrideFcTagColor { get; set; } = false; public bool overrideFcTagColor { get; set; } = false;
public bool useColoredUIDs { get; set; } = true; public bool useColoredUIDs { get; set; } = true;
public bool BroadcastEnabled { get; set; } = false; public bool BroadcastEnabled { get; set; } = false;
public bool LightfinderAutoEnableOnConnect { get; set; } = false;
public short LightfinderLabelOffsetX { get; set; } = 0; public short LightfinderLabelOffsetX { get; set; } = 0;
public short LightfinderLabelOffsetY { get; set; } = 0; public short LightfinderLabelOffsetY { get; set; } = 0;
public bool LightfinderLabelUseIcon { get; set; } = false; public bool LightfinderLabelUseIcon { get; set; } = false;
public bool LightfinderLabelShowOwn { get; set; } = true; public bool LightfinderLabelShowOwn { get; set; } = true;
public bool LightfinderLabelShowPaired { 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 string LightfinderLabelIconGlyph { get; set; } = SeIconCharExtensions.ToIconString(SeIconChar.Hyadelyn);
public float LightfinderLabelScale { get; set; } = 1.0f; public float LightfinderLabelScale { get; set; } = 1.0f;
public bool LightfinderAutoAlign { get; set; } = true; public bool LightfinderAutoAlign { get; set; } = true;

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -1,16 +1,28 @@
namespace LightlessSync.LightlessConfiguration.Models; namespace LightlessSync.LightlessConfiguration.Models;
public enum NotificationLocation public enum NotificationLocation
{ {
Nowhere, Nowhere,
Chat, Chat,
Toast, Toast,
Both Both,
LightlessUi,
ChatAndLightlessUi,
TextOverlay,
} }
public enum NotificationType public enum NotificationType
{ {
Info, Info,
Warning, Warning,
Error Error,
PairRequest,
Download,
Performance
}
public enum NotificationCorner
{
Right,
Left
} }

View 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;
}

View File

@@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<Authors></Authors> <Authors></Authors>
<Company></Company> <Company></Company>
<Version>1.12.1</Version> <Version>1.12.3</Version>
<Description></Description> <Description></Description>
<Copyright></Copyright> <Copyright></Copyright>
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl> <PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>

View File

@@ -98,7 +98,19 @@ public class PlayerDataFactory
private unsafe bool CheckForNullDrawObjectUnsafe(IntPtr playerPointer) private unsafe bool CheckForNullDrawObjectUnsafe(IntPtr playerPointer)
{ {
return ((Character*)playerPointer)->GameObject.DrawObject == null; if (playerPointer == IntPtr.Zero)
return true;
var character = (Character*)playerPointer;
if (character == null)
return true;
var gameObject = &character->GameObject;
if (gameObject == null)
return true;
return gameObject->DrawObject == null;
} }
private async Task<CharacterDataFragment> CreateCharacterData(GameObjectHandler playerRelatedObject, CancellationToken ct) private async Task<CharacterDataFragment> CreateCharacterData(GameObjectHandler playerRelatedObject, CancellationToken ct)

View File

@@ -114,6 +114,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<PluginWarningNotificationService>(); collection.AddSingleton<PluginWarningNotificationService>();
collection.AddSingleton<FileCompactor>(); collection.AddSingleton<FileCompactor>();
collection.AddSingleton<TagHandler>(); collection.AddSingleton<TagHandler>();
collection.AddSingleton(s => new Lazy<ApiController>(() => s.GetRequiredService<ApiController>()));
collection.AddSingleton<PairRequestService>(); collection.AddSingleton<PairRequestService>();
collection.AddSingleton<IdDisplayHandler>(); collection.AddSingleton<IdDisplayHandler>();
collection.AddSingleton<PlayerPerformanceService>(); collection.AddSingleton<PlayerPerformanceService>();
@@ -142,8 +143,18 @@ public sealed class Plugin : IDalamudPlugin
clientState, objectTable, framework, gameGui, condition, gameData, targetManager, gameConfig, clientState, objectTable, framework, gameGui, condition, gameData, targetManager, gameConfig,
s.GetRequiredService<BlockedCharacterHandler>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(), s.GetRequiredService<BlockedCharacterHandler>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(),
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<PlayerPerformanceConfigService>())); s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<PlayerPerformanceConfigService>()));
collection.AddSingleton((s) => new DtrEntry(s.GetRequiredService<ILogger<DtrEntry>>(), dtrBar, s.GetRequiredService<LightlessConfigService>(), collection.AddSingleton((s) => new DtrEntry(
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PairManager>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<ServerConfigurationManager>())); 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>(), collection.AddSingleton(s => new PairManager(s.GetRequiredService<ILogger<PairManager>>(), s.GetRequiredService<PairFactory>(),
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), contextMenu, s.GetRequiredService<PairProcessingLimiter>())); s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), contextMenu, s.GetRequiredService<PairProcessingLimiter>()));
collection.AddSingleton<RedrawManager>(); collection.AddSingleton<RedrawManager>();
@@ -172,9 +183,15 @@ public sealed class Plugin : IDalamudPlugin
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<IpcCallerPenumbra>(), s.GetRequiredService<IpcCallerGlamourer>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<IpcCallerPenumbra>(), s.GetRequiredService<IpcCallerGlamourer>(),
s.GetRequiredService<IpcCallerCustomize>(), s.GetRequiredService<IpcCallerHeels>(), s.GetRequiredService<IpcCallerHonorific>(), s.GetRequiredService<IpcCallerCustomize>(), s.GetRequiredService<IpcCallerHeels>(), s.GetRequiredService<IpcCallerHonorific>(),
s.GetRequiredService<IpcCallerMoodles>(), s.GetRequiredService<IpcCallerPetNames>(), s.GetRequiredService<IpcCallerBrio>())); s.GetRequiredService<IpcCallerMoodles>(), s.GetRequiredService<IpcCallerPetNames>(), s.GetRequiredService<IpcCallerBrio>()));
collection.AddSingleton((s) => new NotificationService(s.GetRequiredService<ILogger<NotificationService>>(), collection.AddSingleton((s) => new NotificationService(
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<ILogger<NotificationService>>(),
notificationManager, chatGui, s.GetRequiredService<LightlessConfigService>())); s.GetRequiredService<LightlessConfigService>(),
s.GetRequiredService<DalamudUtilService>(),
notificationManager,
chatGui,
s.GetRequiredService<LightlessMediator>(),
s.GetRequiredService<PairRequestService>(),
s.GetRequiredService<BroadcastService>()));
collection.AddSingleton((s) => collection.AddSingleton((s) =>
{ {
var httpClient = new HttpClient(); var httpClient = new HttpClient();
@@ -182,10 +199,12 @@ public sealed class Plugin : IDalamudPlugin
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("LightlessSync", ver!.Major + "." + ver!.Minor + "." + ver!.Build)); httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("LightlessSync", ver!.Major + "." + ver!.Minor + "." + ver!.Build));
return httpClient; return httpClient;
}); });
collection.AddSingleton((s) => new UiThemeConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => collection.AddSingleton((s) =>
{ {
var cfg = new LightlessConfigService(pluginInterface.ConfigDirectory.FullName); 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; return cfg;
}); });
collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName));
@@ -197,6 +216,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton((s) => new PlayerPerformanceConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new PlayerPerformanceConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new CharaDataConfigService(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<LightlessConfigService>());
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<UiThemeConfigService>());
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<ServerConfigService>()); collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<ServerConfigService>());
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<NotesConfigService>()); collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<NotesConfigService>());
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<PairTagConfigService>()); collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<PairTagConfigService>());
@@ -235,6 +255,12 @@ public sealed class Plugin : IDalamudPlugin
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, 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<UiSharedService>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<BroadcastScannerService>(), s.GetRequiredService<PairManager>(), s.GetRequiredService<DalamudUtilService>())); 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<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<IPopupHandler, CensusPopupHandler>();
collection.AddScoped<CacheCreationService>(); collection.AddScoped<CacheCreationService>();
collection.AddScoped<PlayerDataFactory>(); collection.AddScoped<PlayerDataFactory>();
@@ -242,7 +268,9 @@ public sealed class Plugin : IDalamudPlugin
collection.AddScoped((s) => new UiService(s.GetRequiredService<ILogger<UiService>>(), pluginInterface.UiBuilder, s.GetRequiredService<LightlessConfigService>(), collection.AddScoped((s) => new UiService(s.GetRequiredService<ILogger<UiService>>(), pluginInterface.UiBuilder, s.GetRequiredService<LightlessConfigService>(),
s.GetRequiredService<WindowSystem>(), s.GetServices<WindowMediatorSubscriberBase>(), s.GetRequiredService<WindowSystem>(), s.GetServices<WindowMediatorSubscriberBase>(),
s.GetRequiredService<UiFactory>(), 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>(), collection.AddScoped((s) => new CommandManagerService(commandManager, s.GetRequiredService<PerformanceCollectorService>(),
s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<CacheMonitor>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<CacheMonitor>(), s.GetRequiredService<ApiController>(),
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<LightlessConfigService>())); s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<LightlessConfigService>()));

View File

@@ -211,6 +211,16 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos
UpdateSyncshellBroadcasts(); 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) protected override void Dispose(bool disposing)
{ {
base.Dispose(disposing); base.Dispose(disposing);

View File

@@ -1,4 +1,4 @@
using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.Group;
using LightlessSync.API.Dto.User; using LightlessSync.API.Dto.User;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
@@ -7,6 +7,7 @@ using LightlessSync.WebAPI;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Threading;
namespace LightlessSync.Services; namespace LightlessSync.Services;
public class BroadcastService : IHostedService, IMediatorSubscriber public class BroadcastService : IHostedService, IMediatorSubscriber
@@ -16,9 +17,11 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
private readonly LightlessMediator _mediator; private readonly LightlessMediator _mediator;
private readonly LightlessConfigService _config; private readonly LightlessConfigService _config;
private readonly DalamudUtilService _dalamudUtil; private readonly DalamudUtilService _dalamudUtil;
private CancellationTokenSource? _lightfinderCancelTokens;
private Action? _connectedHandler;
public LightlessMediator Mediator => _mediator; public LightlessMediator Mediator => _mediator;
public bool IsLightFinderAvailable { get; private set; } = true; public bool IsLightFinderAvailable { get; private set; } = false;
public bool IsBroadcasting => _config.Current.BroadcastEnabled; public bool IsBroadcasting => _config.Current.BroadcastEnabled;
private bool _syncedOnStartup = false; private bool _syncedOnStartup = false;
@@ -57,24 +60,125 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
await action().ConfigureAwait(false); 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<EnableBroadcastMessage>(this, OnEnableBroadcast);
_mediator.Subscribe<BroadcastStatusChangedMessage>(this, OnBroadcastStatusChanged); _mediator.Subscribe<BroadcastStatusChangedMessage>(this, OnBroadcastStatusChanged);
_mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, OnTick); _mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, OnTick);
_mediator.Subscribe<DisconnectedMessage>(this, _ => OnDisconnected());
_apiController.OnConnected += () => _ = CheckLightfinderSupportAsync(cancellationToken); IsLightFinderAvailable = false;
//_ = CheckLightfinderSupportAsync(cancellationToken);
_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) public Task StopAsync(CancellationToken cancellationToken)
{ {
_lightfinderCancelTokens?.Cancel();
_lightfinderCancelTokens?.Dispose();
_lightfinderCancelTokens = null;
if (_connectedHandler is not null)
{
_apiController.OnConnected -= _connectedHandler;
_connectedHandler = null;
}
_mediator.UnsubscribeAll(this); _mediator.UnsubscribeAll(this);
_apiController.OnConnected -= () => _ = CheckLightfinderSupportAsync(cancellationToken);
return Task.CompletedTask; return Task.CompletedTask;
} }
// need to rework this, this is cooked
private async Task CheckLightfinderSupportAsync(CancellationToken cancellationToken) private async Task CheckLightfinderSupportAsync(CancellationToken cancellationToken)
{ {
try try
@@ -85,25 +189,54 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
if (cancellationToken.IsCancellationRequested) if (cancellationToken.IsCancellationRequested)
return; return;
var dummy = "0".PadLeft(64, '0'); var hashedCid = await GetLocalHashedCidAsync("Lightfinder state check").ConfigureAwait(false);
if (string.IsNullOrEmpty(hashedCid))
return;
await _apiController.IsUserBroadcasting(dummy).ConfigureAwait(false); BroadcastStatusInfoDto? status = null;
await _apiController.SetBroadcastStatus(dummy, true, null).ConfigureAwait(false); try
await _apiController.GetBroadcastTtl(dummy).ConfigureAwait(false); {
await _apiController.AreUsersBroadcasting([dummy]).ConfigureAwait(false); 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; 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; bool isBroadcasting = status?.IsBroadcasting == true;
_config.Current.BroadcastTtl = DateTime.MinValue; TimeSpan? ttl = status?.TTL;
_config.Save();
_mediator.Publish(new BroadcastStatusChangedMessage(false, null)); 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) catch (OperationCanceledException)
{ {
@@ -111,14 +244,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogWarning(ex, "Lightfinder check failed."); HandleLightfinderUnavailable("Lightfinder check failed.", ex);
IsLightFinderAvailable = false;
_config.Current.BroadcastEnabled = false;
_config.Current.BroadcastTtl = DateTime.MinValue;
_config.Save();
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
} }
} }
@@ -139,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.LogDebug("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) if (!msg.Enabled)
{ {
_config.Current.BroadcastEnabled = false; ApplyBroadcastDisabled(forcePublish: true);
_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}"))); Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Disabled Lightfinder for Player: {msg.HashedCid}")));
return; return;
} }
_waitingForTtlFetch = true; _waitingForTtlFetch = true;
TimeSpan? ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false); try
if (ttl is { } remaining && remaining > TimeSpan.Zero)
{ {
_config.Current.BroadcastTtl = DateTime.UtcNow + remaining; TimeSpan? ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false);
_config.Current.BroadcastEnabled = true;
_config.Save();
_logger.LogDebug("Fetched TTL from server: {TTL}", remaining); if (TryApplyBroadcastEnabled(ttl, "client request"))
_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}"))); _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."); _waitingForTtlFetch = false;
_config.Current.BroadcastEnabled = false;
_config.Current.BroadcastTtl = DateTime.MinValue;
_config.Save();
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
} }
_waitingForTtlFetch = false;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -219,17 +337,24 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
return result; return result;
} }
public async Task<TimeSpan?> GetBroadcastTtlAsync(string cid) public async Task<TimeSpan?> GetBroadcastTtlAsync(string? cidForLog = null)
{ {
TimeSpan? ttl = null; TimeSpan? ttl = null;
await RequireConnectionAsync(nameof(GetBroadcastTtlAsync), async () => { await RequireConnectionAsync(nameof(GetBroadcastTtlAsync), async () => {
try try
{ {
ttl = await _apiController.GetBroadcastTtl(cid).ConfigureAwait(false); ttl = await _apiController.GetBroadcastTtl().ConfigureAwait(false);
} }
catch (Exception ex) 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); }).ConfigureAwait(false);
return ttl; return ttl;
@@ -269,6 +394,10 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
if (!IsLightFinderAvailable) if (!IsLightFinderAvailable)
{ {
_logger.LogWarning("ToggleBroadcast - Lightfinder is not available."); _logger.LogWarning("ToggleBroadcast - Lightfinder is not available.");
_mediator.Publish(new NotificationMessage(
"Broadcast Unavailable",
"Lightfinder is not available on this server.",
LightlessConfiguration.Models.NotificationType.Error));
return; return;
} }
@@ -278,10 +407,19 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
if (!_config.Current.BroadcastEnabled && cooldown is { } cd && cd > TimeSpan.Zero) if (!_config.Current.BroadcastEnabled && cooldown is { } cd && cd > TimeSpan.Zero)
{ {
_logger.LogWarning("Cooldown active. Must wait {Remaining}s before re-enabling.", cd.TotalSeconds); _logger.LogWarning("Cooldown active. Must wait {Remaining}s before re-enabling.", cd.TotalSeconds);
_mediator.Publish(new NotificationMessage(
"Broadcast Cooldown",
$"Please wait {cd.TotalSeconds:F0} seconds before re-enabling broadcast.",
LightlessConfiguration.Models.NotificationType.Warning));
return; 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 try
{ {
@@ -297,10 +435,19 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
_logger.LogDebug("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)); _mediator.Publish(new EnableBroadcastMessage(hashedCid, newStatus));
_mediator.Publish(new NotificationMessage(
newStatus ? "Broadcast Enabled" : "Broadcast Disabled",
newStatus ? "Your Lightfinder broadcast has been enabled." : "Your Lightfinder broadcast has been disabled.",
LightlessConfiguration.Models.NotificationType.Info));
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Failed to determine current broadcast status for toggle"); _logger.LogError(ex, "Failed to determine current broadcast status for toggle");
_mediator.Publish(new NotificationMessage(
"Broadcast Failed",
$"Failed to toggle broadcast: {ex.Message}",
LightlessConfiguration.Models.NotificationType.Error));
} }
}).ConfigureAwait(false); }).ConfigureAwait(false);
} }
@@ -321,31 +468,31 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
await RequireConnectionAsync(nameof(OnTick), async () => { await RequireConnectionAsync(nameof(OnTick), async () => {
if (!_syncedOnStartup && _config.Current.BroadcastEnabled) if (!_syncedOnStartup && _config.Current.BroadcastEnabled)
{ {
_syncedOnStartup = true;
try try
{ {
string hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); var hashedCid = await GetLocalHashedCidAsync("startup TTL refresh").ConfigureAwait(false);
TimeSpan? ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false); if (string.IsNullOrEmpty(hashedCid))
if (ttl is { }
remaining && remaining > TimeSpan.Zero)
{ {
_config.Current.BroadcastTtl = DateTime.UtcNow + remaining; _logger.LogDebug("Skipping TTL refresh; hashed CID unavailable.");
_config.Current.BroadcastEnabled = true; return;
_config.Save(); }
_logger.LogDebug("Refreshed broadcast TTL from server on first OnTick: {TTL}", remaining);
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 else
{ {
_logger.LogWarning("No valid TTL found on OnTick. Disabling broadcast state."); _logger.LogWarning("No valid TTL found on OnTick. Disabling broadcast state.");
_config.Current.BroadcastEnabled = false; ApplyBroadcastDisabled(forcePublish: true);
_config.Current.BroadcastTtl = DateTime.MinValue;
_config.Save();
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Failed to refresh TTL in OnTick"); _logger.LogError(ex, "Failed to refresh TTL in OnTick");
_syncedOnStartup = false;
} }
} }
if (_config.Current.BroadcastEnabled) if (_config.Current.BroadcastEnabled)
@@ -362,10 +509,8 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
if (_remainingTtl == null) if (_remainingTtl == null)
{ {
_logger.LogDebug("Broadcast TTL expired. Disabling broadcast locally."); _logger.LogDebug("Broadcast TTL expired. Disabling broadcast locally.");
_config.Current.BroadcastEnabled = false; ApplyBroadcastDisabled(forcePublish: true);
_config.Current.BroadcastTtl = DateTime.MinValue; _mediator.Publish(new BroadcastExpiredMessage());
_config.Save();
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
} }
} }
else else

View File

@@ -43,7 +43,7 @@ public sealed class CommandManagerService : IDisposable
"\t /light gpose - Opens the Lightless Character Data Hub window" + 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 analyze - Opens the Lightless Character Data Analysis window" + Environment.NewLine +
"\t /light settings - Opens the Lightless Settings window" + Environment.NewLine + "\t /light settings - Opens the Lightless Settings window" + Environment.NewLine +
"\t /light lightfinder - Opens the Lightfinder window" "\t /light finder - Opens the Lightfinder window"
}); });
} }
@@ -123,7 +123,7 @@ public sealed class CommandManagerService : IDisposable
{ {
_mediator.Publish(new UiToggleMessage(typeof(SettingsUi))); _mediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
} }
else if (string.Equals(splitArgs[0], "lightfinder", StringComparison.OrdinalIgnoreCase)) else if (string.Equals(splitArgs[0], "finder", StringComparison.OrdinalIgnoreCase))
{ {
_mediator.Publish(new UiToggleMessage(typeof(BroadcastUI))); _mediator.Publish(new UiToggleMessage(typeof(BroadcastUI)));
} }

View File

@@ -147,7 +147,7 @@ internal class ContextMenuService : IHostedService
var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address); var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address);
_logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid); _logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid);
await _apiController.TryPairWithContentId(receiverCid, senderCid).ConfigureAwait(false); await _apiController.TryPairWithContentId(receiverCid).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(receiverCid)) if (!string.IsNullOrWhiteSpace(receiverCid))
{ {
_pairRequestService.RemoveRequest(receiverCid); _pairRequestService.RemoveRequest(receiverCid);

View File

@@ -17,6 +17,7 @@ namespace LightlessSync.Services.Mediator;
public record SwitchToIntroUiMessage : MessageBase; public record SwitchToIntroUiMessage : MessageBase;
public record SwitchToMainUiMessage : MessageBase; public record SwitchToMainUiMessage : MessageBase;
public record OpenSettingsUiMessage : MessageBase; public record OpenSettingsUiMessage : MessageBase;
public record OpenLightfinderSettingsMessage : MessageBase;
public record DalamudLoginMessage : MessageBase; public record DalamudLoginMessage : MessageBase;
public record DalamudLogoutMessage : MessageBase; public record DalamudLogoutMessage : MessageBase;
public record PriorityFrameworkUpdateMessage : SameThreadMessage; public record PriorityFrameworkUpdateMessage : SameThreadMessage;
@@ -47,18 +48,23 @@ public record PetNamesMessage(string PetNicknamesData) : MessageBase;
public record HonorificReadyMessage : MessageBase; public record HonorificReadyMessage : MessageBase;
public record TransientResourceChangedMessage(IntPtr Address) : MessageBase; public record TransientResourceChangedMessage(IntPtr Address) : MessageBase;
public record HaltScanMessage(string Source) : MessageBase; public record HaltScanMessage(string Source) : MessageBase;
public record ResumeScanMessage(string Source) : MessageBase;
public record NotificationMessage public record NotificationMessage
(string Title, string Message, NotificationType Type, TimeSpan? TimeShownOnScreen = null) : MessageBase; (string Title, string Message, NotificationType Type, TimeSpan? TimeShownOnScreen = null) : MessageBase;
public record PerformanceNotificationMessage
(string Title, string Message, UserData UserData, bool IsPaused, string PlayerName) : MessageBase;
public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage; public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage;
public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage; public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage;
public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage; public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage;
public record LightlessNotificationMessage(LightlessSync.UI.Models.LightlessNotification Notification) : MessageBase;
public record LightlessNotificationDismissMessage(string NotificationId) : MessageBase;
public record ClearAllNotificationsMessage : MessageBase;
public record CharacterDataAnalyzedMessage : MessageBase; public record CharacterDataAnalyzedMessage : MessageBase;
public record PenumbraStartRedrawMessage(IntPtr Address) : MessageBase; public record PenumbraStartRedrawMessage(IntPtr Address) : MessageBase;
public record PenumbraEndRedrawMessage(IntPtr Address) : MessageBase; public record PenumbraEndRedrawMessage(IntPtr Address) : MessageBase;
public record HubReconnectingMessage(Exception? Exception) : SameThreadMessage; public record HubReconnectingMessage(Exception? Exception) : SameThreadMessage;
public record HubReconnectedMessage(string? Arg) : SameThreadMessage; public record HubReconnectedMessage(string? Arg) : SameThreadMessage;
public record HubClosedMessage(Exception? Exception) : SameThreadMessage; public record HubClosedMessage(Exception? Exception) : SameThreadMessage;
public record ResumeScanMessage(string Source) : MessageBase;
public record DownloadReadyMessage(Guid RequestId) : MessageBase; public record DownloadReadyMessage(Guid RequestId) : MessageBase;
public record DownloadStartedMessage(GameObjectHandler DownloadId, Dictionary<string, FileDownloadStatus> DownloadStatus) : MessageBase; public record DownloadStartedMessage(GameObjectHandler DownloadId, Dictionary<string, FileDownloadStatus> DownloadStatus) : MessageBase;
public record DownloadFinishedMessage(GameObjectHandler DownloadId) : MessageBase; public record DownloadFinishedMessage(GameObjectHandler DownloadId) : MessageBase;
@@ -102,6 +108,8 @@ public record EnableBroadcastMessage(string HashedCid, bool Enabled) : MessageBa
public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl) : MessageBase; public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl) : MessageBase;
public record SyncshellBroadcastsUpdatedMessage : MessageBase; public record SyncshellBroadcastsUpdatedMessage : MessageBase;
public record PairRequestsUpdatedMessage : MessageBase; public record PairRequestsUpdatedMessage : MessageBase;
public record PairRequestReceivedMessage(string SenderName, string SenderId) : MessageBase;
public record BroadcastExpiredMessage : MessageBase;
public record VisibilityChange : MessageBase; public record VisibilityChange : MessageBase;
#pragma warning restore S2094 #pragma warning restore S2094
#pragma warning restore MA0048 // File name must match type name #pragma warning restore MA0048 // File name must match type name

View File

@@ -1,5 +1,4 @@
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using LightlessSync.UI.Style;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace LightlessSync.Services.Mediator; namespace LightlessSync.Services.Mediator;
@@ -34,18 +33,6 @@ public abstract class WindowMediatorSubscriberBase : Window, IMediatorSubscriber
GC.SuppressFinalize(this); 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() public override void Draw()
{ {
_performanceCollectorService.LogPerformance(this, $"Draw", DrawInternal); _performanceCollectorService.LogPerformance(this, $"Draw", DrawInternal);

View File

@@ -221,6 +221,9 @@ public unsafe class NameplateHandler : IMediatorSubscriber
if (pNode == null) if (pNode == null)
continue; continue;
if (mpNameplateAddon == null)
continue;
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)objectInfo->GameObject); var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)objectInfo->GameObject);
if (cid == null || !_activeBroadcastingCids.Contains(cid)) if (cid == null || !_activeBroadcastingCids.Contains(cid))
@@ -247,9 +250,17 @@ public unsafe class NameplateHandler : IMediatorSubscriber
var pNameplateIconNode = nameplateObject.MarkerIcon; var pNameplateIconNode = nameplateObject.MarkerIcon;
var pNameplateResNode = nameplateObject.NameContainer; var pNameplateResNode = nameplateObject.NameContainer;
var pNameplateTextNode = nameplateObject.NameText; var pNameplateTextNode = nameplateObject.NameText;
bool IsVisible = pNameplateIconNode->AtkResNode.IsVisible() || (pNameplateResNode->IsVisible() && pNameplateTextNode->AtkResNode.IsVisible()); bool IsVisible = pNameplateIconNode->AtkResNode.IsVisible() || (pNameplateResNode->IsVisible() && pNameplateTextNode->AtkResNode.IsVisible()) || _configService.Current.LightfinderLabelShowHidden;
pNode->AtkResNode.ToggleVisibility(IsVisible); pNode->AtkResNode.ToggleVisibility(IsVisible);
if (nameplateObject.RootComponentNode == null ||
nameplateObject.NameContainer == null ||
nameplateObject.NameText == null)
{
pNode->AtkResNode.ToggleVisibility(false);
continue;
}
var nameContainer = nameplateObject.NameContainer; var nameContainer = nameplateObject.NameContainer;
var nameText = nameplateObject.NameText; var nameText = nameplateObject.NameText;
@@ -259,8 +270,8 @@ public unsafe class NameplateHandler : IMediatorSubscriber
continue; continue;
} }
var labelColor = UIColors.Get("LightlessPurple"); var labelColor = UIColors.Get("Lightfinder");
var edgeColor = UIColors.Get("FullBlack"); var edgeColor = UIColors.Get("LightfinderEdge");
var config = _configService.Current; var config = _configService.Current;
var scaleMultiplier = System.Math.Clamp(config.LightfinderLabelScale, 0.5f, 2.0f); var scaleMultiplier = System.Math.Clamp(config.LightfinderLabelScale, 0.5f, 2.0f);
@@ -360,33 +371,35 @@ public unsafe class NameplateHandler : IMediatorSubscriber
} }
int positionX; 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) if (config.LightfinderAutoAlign && nameContainer != null && hasValidOffset)
{ {
var nameplateWidth = (int)nameContainer->Width; var nameplateWidth = (int)nameContainer->Width;
if (!config.LightfinderLabelUseIcon)
{
pNode->TextFlags &= ~TextFlags.AutoAdjustNodeSize;
pNode->AtkResNode.Width = 0;
pNode->SetText(labelContent);
nodeWidth = (int)pNode->AtkResNode.GetWidth();
if (nodeWidth <= 0)
nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
if (nodeWidth > nameplateWidth)
nodeWidth = nameplateWidth;
pNode->AtkResNode.Width = (ushort)nodeWidth;
}
else
{
pNode->TextFlags |= TextFlags.AutoAdjustNodeSize;
pNode->AtkResNode.Width = 0;
pNode->SetText(labelContent);
nodeWidth = (int)pNode->AtkResNode.GetWidth();
}
int leftPos = nameplateWidth / 8; int leftPos = nameplateWidth / 8;
int rightPos = nameplateWidth - nodeWidth - (nameplateWidth / 8); int rightPos = nameplateWidth - nodeWidth - (nameplateWidth / 8);
int centrePos = (nameplateWidth - nodeWidth) / 2; int centrePos = (nameplateWidth - nodeWidth) / 2;

View File

@@ -1,102 +1,466 @@
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Interface;
using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Models; using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.UI;
using LightlessSync.UI.Models;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using FFXIVClientStructs.FFXIV.Client.UI;
using LightlessSync.API.Data;
using NotificationType = LightlessSync.LightlessConfiguration.Models.NotificationType; using NotificationType = LightlessSync.LightlessConfiguration.Models.NotificationType;
namespace LightlessSync.Services; namespace LightlessSync.Services;
public class NotificationService : DisposableMediatorSubscriberBase, IHostedService public class NotificationService : DisposableMediatorSubscriberBase, IHostedService
{ {
private readonly ILogger<NotificationService> _logger;
private readonly LightlessConfigService _configService;
private readonly DalamudUtilService _dalamudUtilService; private readonly DalamudUtilService _dalamudUtilService;
private readonly INotificationManager _notificationManager; private readonly INotificationManager _notificationManager;
private readonly IChatGui _chatGui; private readonly IChatGui _chatGui;
private readonly LightlessConfigService _configurationService; private readonly PairRequestService _pairRequestService;
private readonly BroadcastService _broadcastService;
private readonly HashSet<string> _shownPairRequestNotifications = new();
public NotificationService(ILogger<NotificationService> logger, LightlessMediator mediator, public NotificationService(
ILogger<NotificationService> logger,
LightlessConfigService configService,
DalamudUtilService dalamudUtilService, DalamudUtilService dalamudUtilService,
INotificationManager notificationManager, INotificationManager notificationManager,
IChatGui chatGui, LightlessConfigService configurationService) : base(logger, mediator) IChatGui chatGui,
LightlessMediator mediator,
PairRequestService pairRequestService,
BroadcastService broadcastService) : base(logger, mediator)
{ {
_logger = logger;
_configService = configService;
_dalamudUtilService = dalamudUtilService; _dalamudUtilService = dalamudUtilService;
_notificationManager = notificationManager; _notificationManager = notificationManager;
_chatGui = chatGui; _chatGui = chatGui;
_configurationService = configurationService; _pairRequestService = pairRequestService;
_broadcastService = broadcastService;
} }
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
{ {
Mediator.Subscribe<NotificationMessage>(this, ShowNotification); Mediator.Subscribe<NotificationMessage>(this, HandleNotificationMessage);
Mediator.Subscribe<PairRequestsUpdatedMessage>(this, HandlePairRequestsUpdated);
Mediator.Subscribe<PairRequestReceivedMessage>(this, HandlePairRequestReceived);
Mediator.Subscribe<PerformanceNotificationMessage>(this, HandlePerformanceNotification);
Mediator.Subscribe<BroadcastExpiredMessage>(this, HandleBroadcastExpired);
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task StopAsync(CancellationToken cancellationToken) public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
{
return Task.CompletedTask;
}
private void PrintErrorChat(string? message) public void ShowNotification(string title, string message, NotificationType type = NotificationType.Info,
TimeSpan? duration = null, List<LightlessNotificationAction>? actions = null, uint? soundEffectId = null)
{ {
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Error: " + message); var notification = CreateNotification(title, message, type, duration, actions, soundEffectId);
_chatGui.PrintError(se.BuiltString);
}
private void PrintInfoChat(string? message) if (_configService.Current.AutoDismissOnAction && notification.Actions.Any())
{
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Info: ").AddItalics(message ?? string.Empty);
_chatGui.Print(se.BuiltString);
}
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 ShowChat(NotificationMessage msg)
{
switch (msg.Type)
{ {
case NotificationType.Info: WrapActionsWithAutoDismiss(notification);
PrintInfoChat(msg.Message); }
break;
case NotificationType.Warning: if (notification.SoundEffectId.HasValue)
PrintWarnChat(msg.Message); {
break; PlayNotificationSound(notification.SoundEffectId.Value);
}
case NotificationType.Error: Mediator.Publish(new LightlessNotificationMessage(notification));
PrintErrorChat(msg.Message); }
break;
private LightlessNotification CreateNotification(string title, string message, NotificationType type,
TimeSpan? duration, List<LightlessNotificationAction>? actions, uint? soundEffectId)
{
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 WrapActionsWithAutoDismiss(LightlessNotification notification)
{
foreach (var action in notification.Actions)
{
var originalOnClick = action.OnClick;
action.OnClick = (n) =>
{
originalOnClick(n);
if (_configService.Current.AutoDismissOnAction)
{
DismissNotification(n);
}
};
} }
} }
private void ShowNotification(NotificationMessage msg) private void DismissNotification(LightlessNotification notification)
{ {
Logger.LogInformation("{msg}", msg.ToString()); notification.IsDismissed = true;
notification.IsAnimatingOut = true;
}
public void ShowPairRequestNotification(string senderName, string senderId, Action onAccept, Action onDecline)
{
var location = GetNotificationLocation(NotificationType.PairRequest);
// Show in chat if configured
if (location == NotificationLocation.Chat || location == NotificationLocation.ChatAndLightlessUi)
{
ShowChat(new NotificationMessage("Pair Request Received", $"{senderName} wants to directly pair with you.", NotificationType.PairRequest));
}
// Show Lightless notification if configured and action buttons are enabled
if ((location == NotificationLocation.LightlessUi || location == NotificationLocation.ChatAndLightlessUi)
&& _configService.Current.UseLightlessNotifications
&& _configService.Current.ShowPairRequestNotificationActions)
{
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));
}
else if (location != NotificationLocation.Nowhere && location != NotificationLocation.Chat)
{
// Fall back to regular notification without action buttons
HandleNotificationMessage(new NotificationMessage("Pair Request Received", $"{senderName} wants to directly pair with you.", NotificationType.PairRequest));
}
}
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),
NotificationType.Performance => TimeSpan.FromSeconds(_configService.Current.PerformanceNotificationDurationSeconds),
_ => 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.Performance => _configService.Current.DisablePerformanceSound,
NotificationType.Download => true, // Download sounds always disabled
_ => false
};
private uint GetConfiguredSoundForType(NotificationType type) => type switch
{
NotificationType.Info => _configService.Current.CustomInfoSoundId,
NotificationType.Warning => _configService.Current.CustomWarningSoundId,
NotificationType.Error => _configService.Current.CustomErrorSoundId,
NotificationType.Performance => _configService.Current.PerformanceSoundId,
_ => 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; if (!_dalamudUtilService.IsLoggedIn) return;
switch (msg.Type) var location = GetNotificationLocation(msg.Type);
{ ShowNotificationLocationBased(msg, location);
case NotificationType.Info:
ShowNotificationLocationBased(msg, _configurationService.Current.InfoNotification);
break;
case NotificationType.Warning:
ShowNotificationLocationBased(msg, _configurationService.Current.WarningNotification);
break;
case NotificationType.Error:
ShowNotificationLocationBased(msg, _configurationService.Current.ErrorNotification);
break;
}
} }
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,
NotificationType.Performance => _configService.Current.LightlessPerformanceNotification,
_ => 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) private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location)
{ {
switch (location) switch (location)
@@ -114,20 +478,29 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
ShowChat(msg); ShowChat(msg);
break; break;
case NotificationLocation.LightlessUi:
ShowLightlessNotification(msg);
break;
case NotificationLocation.ChatAndLightlessUi:
ShowChat(msg);
ShowLightlessNotification(msg);
break;
case NotificationLocation.Nowhere: case NotificationLocation.Nowhere:
break; 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) private void ShowToast(NotificationMessage msg)
{ {
Dalamud.Interface.ImGuiNotification.NotificationType dalamudType = msg.Type switch var dalamudType = ConvertToDalamudNotificationType(msg.Type);
{
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() _notificationManager.AddNotification(new Notification()
{ {
@@ -138,4 +511,321 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
InitialDuration = msg.TimeShownOnScreen ?? TimeSpan.FromSeconds(3) 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)
{
case NotificationType.Info:
PrintInfoChat(msg.Message);
break;
case NotificationType.Warning:
PrintWarnChat(msg.Message);
break;
case NotificationType.Error:
PrintErrorChat(msg.Message);
break;
case NotificationType.PairRequest:
PrintPairRequestChat(msg.Title, msg.Message);
break;
case NotificationType.Performance:
PrintPerformanceChat(msg.Title, msg.Message);
break;
// Download notifications don't support chat output, will be a giga spam otherwise
case NotificationType.Download:
break;
}
}
private void PrintErrorChat(string? message)
{
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Error: " + message);
_chatGui.PrintError(se.BuiltString);
}
private void PrintInfoChat(string? message)
{
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Info: ")
.AddItalics(message ?? string.Empty);
_chatGui.Print(se.BuiltString);
}
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 PrintPairRequestChat(string? title, string? message)
{
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ")
.AddUiForeground("Pair Request: ", 541).AddUiForegroundOff()
.AddText(title ?? message ?? string.Empty);
_chatGui.Print(se.BuiltString);
}
private void PrintPerformanceChat(string? title, string? message)
{
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ")
.AddUiForeground("Performance: ", 508).AddUiForegroundOff()
.AddText(title ?? message ?? string.Empty);
_chatGui.Print(se.BuiltString);
}
private void HandlePairRequestReceived(PairRequestReceivedMessage msg)
{
ShowPairRequestNotification(
msg.SenderName,
msg.SenderId,
() => _pairRequestService.AcceptPairRequest(msg.SenderId, msg.SenderName),
() => _pairRequestService.DeclinePairRequest(msg.SenderId)
);
}
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)
{
var notificationId = $"pair_request_{hashedCid}";
Mediator.Publish(new LightlessNotificationDismissMessage(notificationId));
_shownPairRequestNotifications.Remove(hashedCid);
}
// Track active requests
foreach (var request in activeRequests)
{
_shownPairRequestNotifications.Add(request.HashedCid);
}
}
private void HandlePerformanceNotification(PerformanceNotificationMessage msg)
{
var location = GetNotificationLocation(NotificationType.Performance);
// Show in chat if configured
if (location == NotificationLocation.Chat || location == NotificationLocation.ChatAndLightlessUi)
{
ShowChat(new NotificationMessage(msg.Title, msg.Message, NotificationType.Performance));
}
// Show Lightless notification if configured and action buttons are enabled
if ((location == NotificationLocation.LightlessUi || location == NotificationLocation.ChatAndLightlessUi)
&& _configService.Current.UseLightlessNotifications
&& _configService.Current.ShowPerformanceNotificationActions)
{
var actions = CreatePerformanceActions(msg.UserData, msg.IsPaused, msg.PlayerName);
var notification = new LightlessNotification
{
Title = msg.Title,
Message = msg.Message,
Type = NotificationType.Performance,
Duration = TimeSpan.FromSeconds(_configService.Current.PerformanceNotificationDurationSeconds),
Actions = actions,
SoundEffectId = GetSoundEffectId(NotificationType.Performance, null)
};
if (notification.SoundEffectId.HasValue)
{
PlayNotificationSound(notification.SoundEffectId.Value);
}
Mediator.Publish(new LightlessNotificationMessage(notification));
}
else if (location != NotificationLocation.Nowhere && location != NotificationLocation.Chat)
{
// Fall back to regular notification without action buttons
HandleNotificationMessage(new NotificationMessage(msg.Title, msg.Message, NotificationType.Performance));
}
}
private List<LightlessNotificationAction> CreatePerformanceActions(UserData userData, bool isPaused, string playerName)
{
var actions = new List<LightlessNotificationAction>();
if (isPaused)
{
actions.Add(new LightlessNotificationAction
{
Label = "Unpause",
Icon = FontAwesomeIcon.Play,
Color = UIColors.Get("LightlessGreen"),
IsPrimary = true,
OnClick = (notification) =>
{
try
{
Mediator.Publish(new CyclePauseMessage(userData));
DismissNotification(notification);
var displayName = GetUserDisplayName(userData, playerName);
ShowNotification(
"Player Unpaused",
$"Successfully unpaused {displayName}",
NotificationType.Info,
TimeSpan.FromSeconds(3));
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to unpause player {uid}", userData.UID);
var displayName = GetUserDisplayName(userData, playerName);
ShowNotification(
"Unpause Failed",
$"Failed to unpause {displayName}",
NotificationType.Error,
TimeSpan.FromSeconds(5));
}
}
});
}
else
{
actions.Add(new LightlessNotificationAction
{
Label = "Pause",
Icon = FontAwesomeIcon.Pause,
Color = UIColors.Get("LightlessOrange"),
IsPrimary = true,
OnClick = (notification) =>
{
try
{
Mediator.Publish(new PauseMessage(userData));
DismissNotification(notification);
var displayName = GetUserDisplayName(userData, playerName);
ShowNotification(
"Player Paused",
$"Successfully paused {displayName}",
NotificationType.Info,
TimeSpan.FromSeconds(3));
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to pause player {uid}", userData.UID);
var displayName = GetUserDisplayName(userData, playerName);
ShowNotification(
"Pause Failed",
$"Failed to pause {displayName}",
NotificationType.Error,
TimeSpan.FromSeconds(5));
}
}
});
}
// Add dismiss button
actions.Add(new LightlessNotificationAction
{
Label = "Dismiss",
Icon = FontAwesomeIcon.Times,
Color = UIColors.Get("DimRed"),
IsPrimary = false,
OnClick = (notification) =>
{
DismissNotification(notification);
}
});
return actions;
}
private string GetUserDisplayName(UserData userData, string playerName)
{
if (!string.IsNullOrEmpty(userData.Alias) && !string.Equals(userData.Alias, userData.UID, StringComparison.Ordinal))
{
return $"{playerName} ({userData.Alias})";
}
return $"{playerName} ({userData.UID})";
}
private void HandleBroadcastExpired(BroadcastExpiredMessage _)
{
var location = GetNotificationLocation(NotificationType.Warning);
if (location == NotificationLocation.Chat || location == NotificationLocation.ChatAndLightlessUi)
{
ShowChat(new NotificationMessage("Broadcast Expired", "Your Lightfinder broadcast has expired after 3 hours.", NotificationType.Warning));
}
if ((location == NotificationLocation.LightlessUi || location == NotificationLocation.ChatAndLightlessUi)
&& _configService.Current.UseLightlessNotifications)
{
var notification = new LightlessNotification
{
Id = "broadcast_expired",
Title = "Broadcast Expired",
Message = "Your Lightfinder broadcast has expired after 3 hours. Would you like to re-enable it?",
Type = NotificationType.Warning,
Duration = TimeSpan.FromSeconds(_configService.Current.WarningNotificationDurationSeconds),
SoundEffectId = GetSoundEffectId(NotificationType.Warning, null),
Actions = CreateBroadcastExpiredActions()
};
if (notification.SoundEffectId.HasValue)
{
PlayNotificationSound(notification.SoundEffectId.Value);
}
Mediator.Publish(new LightlessNotificationMessage(notification));
}
else if (location != NotificationLocation.Nowhere && location != NotificationLocation.Chat)
{
HandleNotificationMessage(new NotificationMessage("Broadcast Expired", "Your Lightfinder broadcast has expired after 3 hours.", NotificationType.Warning));
}
}
private List<LightlessNotificationAction> CreateBroadcastExpiredActions()
{
return new List<LightlessNotificationAction>
{
new()
{
Id = "re_enable",
Label = "Re-enable Broadcast",
Icon = FontAwesomeIcon.Plus,
Color = UIColors.Get("LightlessGreen"),
IsPrimary = true,
OnClick = (n) =>
{
_logger.LogInformation("Re-enabling broadcast from notification");
_broadcastService.ToggleBroadcast();
DismissNotification(n);
}
},
new()
{
Id = "close",
Label = "Close",
Icon = FontAwesomeIcon.Times,
Color = UIColors.Get("DimRed"),
OnClick = (n) =>
{
_logger.LogInformation("Broadcast expiration notification dismissed");
DismissNotification(n);
}
}
};
}
} }

View File

@@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.PlayerData.Pairs; using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -11,16 +13,18 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase
{ {
private readonly DalamudUtilService _dalamudUtil; private readonly DalamudUtilService _dalamudUtil;
private readonly PairManager _pairManager; private readonly PairManager _pairManager;
private readonly Lazy<WebAPI.ApiController> _apiController;
private readonly object _syncRoot = new(); private readonly object _syncRoot = new();
private readonly List<PairRequestEntry> _requests = []; private readonly List<PairRequestEntry> _requests = [];
private static readonly TimeSpan Expiration = TimeSpan.FromMinutes(5); private static readonly TimeSpan Expiration = TimeSpan.FromMinutes(5);
public PairRequestService(ILogger<PairRequestService> logger, LightlessMediator mediator, DalamudUtilService dalamudUtil, PairManager pairManager) public PairRequestService(ILogger<PairRequestService> logger, LightlessMediator mediator, DalamudUtilService dalamudUtil, PairManager pairManager, Lazy<WebAPI.ApiController> apiController)
: base(logger, mediator) : base(logger, mediator)
{ {
_dalamudUtil = dalamudUtil; _dalamudUtil = dalamudUtil;
_pairManager = pairManager; _pairManager = pairManager;
_apiController = apiController;
Mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, _ => Mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, _ =>
{ {
@@ -66,6 +70,10 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase
: _dalamudUtil.RunOnFrameworkThread(() => ToDisplay(entry)).GetAwaiter().GetResult(); : _dalamudUtil.RunOnFrameworkThread(() => ToDisplay(entry)).GetAwaiter().GetResult();
Mediator.Publish(new PairRequestsUpdatedMessage()); Mediator.Publish(new PairRequestsUpdatedMessage());
var senderName = string.IsNullOrEmpty(display.DisplayName) ? "Unknown User" : display.DisplayName;
Mediator.Publish(new PairRequestReceivedMessage(senderName, display.HashedCid));
return display; return display;
} }
@@ -183,6 +191,40 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase
return _requests.RemoveAll(r => now - r.ReceivedAt > Expiration) > 0; 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); private record struct PairRequestEntry(string HashedCid, string MessageTemplate, DateTime ReceivedAt);
public readonly record struct PairRequestDisplay(string HashedCid, string DisplayName, string Message, DateTime ReceivedAt); public readonly record struct PairRequestDisplay(string HashedCid, string DisplayName, string Message, DateTime ReceivedAt);

View File

@@ -78,23 +78,26 @@ public class PlayerPerformanceService
string warningText = string.Empty; string warningText = string.Empty;
if (exceedsTris && !exceedsVram) if (exceedsTris && !exceedsVram)
{ {
warningText = $"Player {pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds your configured triangle warning threshold (" + warningText = $"Player {pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds your configured triangle warning threshold\n" +
$"{triUsage}/{config.TrisWarningThresholdThousands * 1000} triangles)."; $"{triUsage}/{config.TrisWarningThresholdThousands * 1000} triangles";
} }
else if (!exceedsTris) else if (!exceedsTris)
{ {
warningText = $"Player {pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds your configured VRAM warning threshold (" + warningText = $"Player {pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds your configured VRAM warning threshold\n" +
$"{UiSharedService.ByteToString(vramUsage, true)}/{config.VRAMSizeWarningThresholdMiB} MiB)."; $"{UiSharedService.ByteToString(vramUsage, true)}/{config.VRAMSizeWarningThresholdMiB} MiB";
} }
else else
{ {
warningText = $"Player {pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds both VRAM warning threshold (" + warningText = $"Player {pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds both VRAM warning threshold and triangle warning threshold\n" +
$"{UiSharedService.ByteToString(vramUsage, true)}/{config.VRAMSizeWarningThresholdMiB} MiB) and " + $"{UiSharedService.ByteToString(vramUsage, true)}/{config.VRAMSizeWarningThresholdMiB} MiB and {triUsage}/{config.TrisWarningThresholdThousands * 1000} triangles";
$"triangle warning threshold ({triUsage}/{config.TrisWarningThresholdThousands * 1000} triangles).";
} }
_mediator.Publish(new NotificationMessage($"{pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds performance threshold(s)", _mediator.Publish(new PerformanceNotificationMessage(
warningText, LightlessConfiguration.Models.NotificationType.Warning)); $"{pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds performance threshold(s)",
warningText,
pairHandler.Pair.UserData,
pairHandler.Pair.IsPaused,
pairHandler.Pair.PlayerName));
} }
return true; return true;
@@ -138,11 +141,15 @@ public class PlayerPerformanceService
if (CheckForThreshold(config.AutoPausePlayersExceedingThresholds, config.TrisAutoPauseThresholdThousands * 1000, if (CheckForThreshold(config.AutoPausePlayersExceedingThresholds, config.TrisAutoPauseThresholdThousands * 1000,
triUsage, config.AutoPausePlayersWithPreferredPermissionsExceedingThresholds, isPrefPerm)) triUsage, config.AutoPausePlayersWithPreferredPermissionsExceedingThresholds, isPrefPerm))
{ {
_mediator.Publish(new NotificationMessage($"{pair.PlayerName} ({pair.UserData.AliasOrUID}) automatically paused", var message = $"Player {pair.PlayerName} ({pair.UserData.AliasOrUID}) exceeded your configured triangle auto pause threshold and has been automatically paused\n" +
$"Player {pair.PlayerName} ({pair.UserData.AliasOrUID}) exceeded your configured triangle auto pause threshold (" + $"{triUsage}/{config.TrisAutoPauseThresholdThousands * 1000} triangles";
$"{triUsage}/{config.TrisAutoPauseThresholdThousands * 1000} triangles)" +
$" and has been automatically paused.", _mediator.Publish(new PerformanceNotificationMessage(
LightlessConfiguration.Models.NotificationType.Warning)); $"{pair.PlayerName} ({pair.UserData.AliasOrUID}) automatically paused",
message,
pair.UserData,
true,
pair.PlayerName));
_mediator.Publish(new EventMessage(new Event(pair.PlayerName, pair.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning, _mediator.Publish(new EventMessage(new Event(pair.PlayerName, pair.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning,
$"Exceeds triangle threshold: automatically paused ({triUsage}/{config.TrisAutoPauseThresholdThousands * 1000} triangles)"))); $"Exceeds triangle threshold: automatically paused ({triUsage}/{config.TrisAutoPauseThresholdThousands * 1000} triangles)")));
@@ -214,11 +221,15 @@ public class PlayerPerformanceService
if (CheckForThreshold(config.AutoPausePlayersExceedingThresholds, config.VRAMSizeAutoPauseThresholdMiB * 1024 * 1024, if (CheckForThreshold(config.AutoPausePlayersExceedingThresholds, config.VRAMSizeAutoPauseThresholdMiB * 1024 * 1024,
vramUsage, config.AutoPausePlayersWithPreferredPermissionsExceedingThresholds, isPrefPerm)) vramUsage, config.AutoPausePlayersWithPreferredPermissionsExceedingThresholds, isPrefPerm))
{ {
_mediator.Publish(new NotificationMessage($"{pair.PlayerName} ({pair.UserData.AliasOrUID}) automatically paused", var message = $"Player {pair.PlayerName} ({pair.UserData.AliasOrUID}) exceeded your configured VRAM auto pause threshold and has been automatically paused\n" +
$"Player {pair.PlayerName} ({pair.UserData.AliasOrUID}) exceeded your configured VRAM auto pause threshold (" + $"{UiSharedService.ByteToString(vramUsage, addSuffix: true)}/{config.VRAMSizeAutoPauseThresholdMiB}MiB";
$"{UiSharedService.ByteToString(vramUsage, addSuffix: true)}/{config.VRAMSizeAutoPauseThresholdMiB}MiB)" +
$" and has been automatically paused.", _mediator.Publish(new PerformanceNotificationMessage(
LightlessConfiguration.Models.NotificationType.Warning)); $"{pair.PlayerName} ({pair.UserData.AliasOrUID}) automatically paused",
message,
pair.UserData,
true,
pair.PlayerName));
_mediator.Publish(new PauseMessage(pair.UserData)); _mediator.Publish(new PauseMessage(pair.UserData));

View File

@@ -607,8 +607,9 @@ public class ServerConfigurationManager
{ {
var baseUri = serverUri.Replace("wss://", "https://").Replace("ws://", "http://"); var baseUri = serverUri.Replace("wss://", "https://").Replace("ws://", "http://");
var oauthCheckUri = LightlessAuth.GetUIDsFullPath(new Uri(baseUri)); var oauthCheckUri = LightlessAuth.GetUIDsFullPath(new Uri(baseUri));
_httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); using var request = new HttpRequestMessage(HttpMethod.Get, oauthCheckUri);
var response = await _httpClient.GetAsync(oauthCheckUri).ConfigureAwait(false); 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); var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
return await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(responseStream).ConfigureAwait(false) ?? []; return await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(responseStream).ConfigureAwait(false) ?? [];
} }

View File

@@ -1,9 +1,10 @@
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.UI; using LightlessSync.UI;
using LightlessSync.UI.Style;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace LightlessSync.Services; namespace LightlessSync.Services;
@@ -22,7 +23,8 @@ public sealed class UiService : DisposableMediatorSubscriberBase
LightlessConfigService lightlessConfigService, WindowSystem windowSystem, LightlessConfigService lightlessConfigService, WindowSystem windowSystem,
IEnumerable<WindowMediatorSubscriberBase> windows, IEnumerable<WindowMediatorSubscriberBase> windows,
UiFactory uiFactory, FileDialogManager fileDialogManager, UiFactory uiFactory, FileDialogManager fileDialogManager,
LightlessMediator lightlessMediator) : base(logger, lightlessMediator) LightlessMediator lightlessMediator,
NotificationService notificationService) : base(logger, lightlessMediator)
{ {
_logger = logger; _logger = logger;
_logger.LogTrace("Creating {type}", GetType().Name); _logger.LogTrace("Creating {type}", GetType().Name);
@@ -119,7 +121,15 @@ public sealed class UiService : DisposableMediatorSubscriberBase
private void Draw() private void Draw()
{ {
_windowSystem.Draw(); MainStyle.PushStyle();
_fileDialogManager.Draw(); try
{
_windowSystem.Draw();
_fileDialogManager.Draw();
}
finally
{
MainStyle.PopStyle();
}
} }
} }

View File

@@ -1,4 +1,5 @@
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Utility; using Dalamud.Utility;
@@ -209,7 +210,7 @@ namespace LightlessSync.UI
else else
{ {
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
ImGui.Text("The Lightfinder<EFBFBD>s light wanes, but not in vain."); // cringe.. ImGui.Text("The Lightfinders light wanes, but not in vain."); // cringe..
ImGui.PopStyleColor(); ImGui.PopStyleColor();
} }
} }
@@ -252,12 +253,27 @@ namespace LightlessSync.UI
_broadcastService.ToggleBroadcast(); _broadcastService.ToggleBroadcast();
} }
var toggleButtonHeight = ImGui.GetItemRectSize().Y;
if (isOnCooldown || !_broadcastService.IsLightFinderAvailable) if (isOnCooldown || !_broadcastService.IsLightFinderAvailable)
ImGui.EndDisabled(); ImGui.EndDisabled();
ImGui.PopStyleColor(); ImGui.PopStyleColor();
ImGui.PopStyleVar(); 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(); ImGui.EndTabItem();
} }

View File

@@ -1,4 +1,4 @@
using System; using System;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
@@ -87,7 +87,7 @@ public class CompactUi : WindowMediatorSubscriberBase
IpcManager ipcManager, IpcManager ipcManager,
BroadcastService broadcastService, BroadcastService broadcastService,
CharacterAnalyzer characterAnalyzer, CharacterAnalyzer characterAnalyzer,
PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService) PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService)
{ {
_uiSharedService = uiShared; _uiSharedService = uiShared;
_configService = configService; _configService = configService;
@@ -105,7 +105,7 @@ public class CompactUi : WindowMediatorSubscriberBase
_renamePairTagUi = renameTagUi; _renamePairTagUi = renameTagUi;
_ipcManager = ipcManager; _ipcManager = ipcManager;
_broadcastService = broadcastService; _broadcastService = broadcastService;
_tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService, dalamudUtilService); _tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService, dalamudUtilService, lightlessNotificationService);
AllowPinning = true; AllowPinning = true;
AllowClickthrough = false; AllowClickthrough = false;

View File

@@ -1,7 +1,7 @@
using System;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.PlayerData.Handlers; using LightlessSync.PlayerData.Handlers;
using LightlessSync.Services; using LightlessSync.Services;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
@@ -22,9 +22,13 @@ public class DownloadUi : WindowMediatorSubscriberBase
private readonly UiSharedService _uiShared; private readonly UiSharedService _uiShared;
private readonly PairProcessingLimiter _pairProcessingLimiter; private readonly PairProcessingLimiter _pairProcessingLimiter;
private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new(); private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new();
private readonly NotificationService _notificationService;
private bool _notificationDismissed = true;
private int _lastDownloadStateHash = 0;
public DownloadUi(ILogger<DownloadUi> logger, DalamudUtilService dalamudUtilService, LightlessConfigService configService, public DownloadUi(ILogger<DownloadUi> logger, DalamudUtilService dalamudUtilService, LightlessConfigService configService,
PairProcessingLimiter pairProcessingLimiter, 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) : base(logger, mediator, "Lightless Sync Downloads", performanceCollectorService)
{ {
_dalamudUtilService = dalamudUtilService; _dalamudUtilService = dalamudUtilService;
@@ -32,6 +36,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
_pairProcessingLimiter = pairProcessingLimiter; _pairProcessingLimiter = pairProcessingLimiter;
_fileTransferManager = fileTransferManager; _fileTransferManager = fileTransferManager;
_uiShared = uiShared; _uiShared = uiShared;
_notificationService = notificationService;
SizeConstraints = new WindowSizeConstraints() SizeConstraints = new WindowSizeConstraints()
{ {
@@ -56,7 +61,14 @@ public class DownloadUi : WindowMediatorSubscriberBase
IsOpen = true; IsOpen = true;
Mediator.Subscribe<DownloadStartedMessage>(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus); 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())
{
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
}
});
Mediator.Subscribe<GposeStartMessage>(this, (_) => IsOpen = false); Mediator.Subscribe<GposeStartMessage>(this, (_) => IsOpen = false);
Mediator.Subscribe<GposeEndMessage>(this, (_) => IsOpen = true); Mediator.Subscribe<GposeEndMessage>(this, (_) => IsOpen = true);
Mediator.Subscribe<PlayerUploadingMessage>(this, (msg) => Mediator.Subscribe<PlayerUploadingMessage>(this, (msg) =>
@@ -77,19 +89,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
if (_configService.Current.ShowTransferWindow) if (_configService.Current.ShowTransferWindow)
{ {
var limiterSnapshot = _pairProcessingLimiter.GetSnapshot(); var limiterSnapshot = _pairProcessingLimiter.GetSnapshot();
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();
}
try try
{ {
if (_fileTransferManager.IsUploading) if (_fileTransferManager.IsUploading)
@@ -117,38 +117,76 @@ public class DownloadUi : WindowMediatorSubscriberBase
} }
catch catch
{ {
// ignore errors thrown from UI _logger.LogDebug("Error drawing upload progress");
} }
try try
{ {
foreach (var item in _currentDownloads.ToList()) // Check if download notifications are enabled (not set to TextOverlay)
{ var useNotifications = _configService.Current.UseLightlessNotifications
var dlSlot = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForSlot); ? _configService.Current.LightlessDownloadNotification != NotificationLocation.TextOverlay
var dlQueue = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForQueue); : _configService.Current.UseNotificationsForDownloads;
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); if (useNotifications)
ImGui.SameLine(); {
var xDistance = ImGui.GetCursorPosX(); // Use notification system
UiSharedService.DrawOutlinedFont( if (_currentDownloads.Any())
$"{item.Key.Name} [W:{dlSlot}/Q:{dlQueue}/P:{dlProg}/D:{dlDecomp}]", {
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); UpdateDownloadNotificationIfChanged(limiterSnapshot);
ImGui.NewLine(); _notificationDismissed = false;
ImGui.SameLine(xDistance); }
UiSharedService.DrawOutlinedFont( else if (!_notificationDismissed)
$"{transferredFiles}/{totalFiles} ({UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)})", {
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
_notificationDismissed = true;
_lastDownloadStateHash = 0;
}
}
else
{
// Use text overlay
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);
}
} }
} }
catch catch
{ {
// ignore errors thrown from UI _logger.LogDebug("Error drawing download progress");
} }
} }
@@ -220,7 +258,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
} }
catch catch
{ {
// ignore errors thrown on UI _logger.LogDebug("Error drawing upload progress");
} }
} }
} }
@@ -262,4 +300,68 @@ public class DownloadUi : WindowMediatorSubscriberBase
MaximumSize = new Vector2(300, maxHeight), MaximumSize = new Vector2(300, maxHeight),
}; };
} }
private void UpdateDownloadNotificationIfChanged(PairProcessingLimiterSnapshot limiterSnapshot)
{
var downloadStatus = new List<(string playerName, float progress, string status)>(_currentDownloads.Count);
var hashCode = new HashCode();
foreach (var item in _currentDownloads)
{
var dlSlot = 0;
var dlQueue = 0;
var dlProg = 0;
var dlDecomp = 0;
long totalBytes = 0;
long transferredBytes = 0;
// Single pass through the dictionary to count everything - avoid multiple LINQ iterations
foreach (var entry in item.Value)
{
var fileStatus = entry.Value;
switch (fileStatus.DownloadStatus)
{
case DownloadStatus.WaitingForSlot: dlSlot++; break;
case DownloadStatus.WaitingForQueue: dlQueue++; break;
case DownloadStatus.Downloading: dlProg++; break;
case DownloadStatus.Decompressing: dlDecomp++; break;
}
totalBytes += fileStatus.TotalBytes;
transferredBytes += fileStatus.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));
// Build hash from meaningful state
hashCode.Add(item.Key.Name);
hashCode.Add(transferredBytes);
hashCode.Add(totalBytes);
hashCode.Add(status);
}
var queueWaiting = limiterSnapshot.IsEnabled ? limiterSnapshot.Waiting : 0;
hashCode.Add(queueWaiting);
var currentHash = hashCode.ToHashCode();
// Only update notification if state has actually changed
if (currentHash != _lastDownloadStateHash)
{
_lastDownloadStateHash = currentHash;
if (downloadStatus.Count > 0 || queueWaiting > 0)
{
_notificationService.ShowPairDownloadNotification(downloadStatus, queueWaiting);
}
}
}
} }

View File

@@ -1,56 +1,92 @@
using Dalamud.Game.Gui.Dtr; using Dalamud.Game.Gui.Dtr;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Configurations; using LightlessSync.LightlessConfiguration.Configurations;
using LightlessSync.PlayerData.Pairs; using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration; using LightlessSync.Services.ServerConfiguration;
using LightlessSync.WebAPI; using LightlessSync.WebAPI;
using LightlessSync.WebAPI.SignalR.Utils; using LightlessSync.WebAPI.SignalR.Utils;
using LightlessSync.Utils;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text;
namespace LightlessSync.UI; namespace LightlessSync.UI;
public sealed class DtrEntry : IDisposable, IHostedService 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 ApiController _apiController;
private readonly ServerConfigurationManager _serverManager; private readonly ServerConfigurationManager _serverManager;
private readonly CancellationTokenSource _cancellationTokenSource = new(); private readonly CancellationTokenSource _cancellationTokenSource = new();
private readonly ConfigurationServiceBase<LightlessConfig> _configService; private readonly ConfigurationServiceBase<LightlessConfig> _configService;
private readonly IDtrBar _dtrBar; 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 ILogger<DtrEntry> _logger;
private readonly BroadcastService _broadcastService;
private readonly BroadcastScannerService _broadcastScannerService;
private readonly LightlessMediator _lightlessMediator; private readonly LightlessMediator _lightlessMediator;
private readonly PairManager _pairManager; private readonly PairManager _pairManager;
private readonly PairRequestService _pairRequestService;
private readonly DalamudUtilService _dalamudUtilService;
private Task? _runTask; private Task? _runTask;
private string? _text; private string? _statusText;
private string? _tooltip; private string? _statusTooltip;
private Colors _colors; 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; _logger = logger;
_dtrBar = dtrBar; _dtrBar = dtrBar;
_entry = new(CreateEntry); _statusEntry = new(CreateStatusEntry);
_lightfinderEntry = new(CreateLightfinderEntry);
_configService = configService; _configService = configService;
_lightlessMediator = lightlessMediator; _lightlessMediator = lightlessMediator;
_pairManager = pairManager; _pairManager = pairManager;
_pairRequestService = pairRequestService;
_apiController = apiController; _apiController = apiController;
_serverManager = serverManager; _serverManager = serverManager;
_broadcastService = broadcastService;
_broadcastScannerService = broadcastScannerService;
_dalamudUtilService = dalamudUtilService;
} }
public void Dispose() public void Dispose()
{ {
if (_entry.IsValueCreated) if (_statusEntry.IsValueCreated)
{ {
_logger.LogDebug("Disposing DtrEntry"); _logger.LogDebug("Disposing DtrEntry");
Clear(); Clear();
_entry.Value.Remove(); _statusEntry.Value.Remove();
} }
if (_lightfinderEntry.IsValueCreated)
_lightfinderEntry.Value.Remove();
} }
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
@@ -70,7 +106,7 @@ public sealed class DtrEntry : IDisposable, IHostedService
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
// ignore cancelled
} }
finally finally
{ {
@@ -80,33 +116,66 @@ public sealed class DtrEntry : IDisposable, IHostedService
private void Clear() private void Clear()
{ {
if (!_entry.IsValueCreated) return; HideStatusEntry();
_logger.LogInformation("Clearing entry"); HideLightfinderEntry();
_text = null;
_tooltip = null;
_colors = default;
_entry.Value.Shown = false;
} }
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"); var entry = _dtrBar.Get("Lightless Sync");
entry.OnClick = interactionEvent => OnClickEvent(interactionEvent); entry.OnClick = interactionEvent => OnStatusEntryClick(interactionEvent);
return entry; 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))); if (interactionEvent.ModifierKeys.HasFlag(ClickModifierKeys.Shift))
} {
else if (interactionEvent.ClickType.Equals(MouseClickType.Left) && interactionEvent.ModifierKeys.Equals(ClickModifierKeys.Shift)) _lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
{ }
_lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi))); else
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(CompactUi)));
}
return;
} }
if (interactionEvent.ClickType.Equals(MouseClickType.Right)) 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() private async Task RunAsync()
{ {
while (!_cancellationTokenSource.IsCancellationRequested) while (!_cancellationTokenSource.IsCancellationRequested)
@@ -143,96 +223,278 @@ public sealed class DtrEntry : IDisposable, IHostedService
private void Update() private void Update()
{ {
if (!_configService.Current.EnableDtrEntry || !_configService.Current.HasValidSetup()) var config = _configService.Current;
{
if (_entry.IsValueCreated && _entry.Value.Shown)
{
_logger.LogInformation("Disabling entry");
Clear(); if (!config.HasValidSetup())
} {
HideStatusEntry();
HideLightfinderEntry();
return; return;
} }
if (!_entry.Value.Shown) if (config.EnableDtrEntry)
{ UpdateStatusEntry(config);
_logger.LogInformation("Showing entry"); else
_entry.Value.Shown = true; HideStatusEntry();
}
if (config.ShowLightfinderInDtr)
UpdateLightfinderEntry(config);
else
HideLightfinderEntry();
}
private void UpdateStatusEntry(LightlessConfig config)
{
string text; string text;
string tooltip; string tooltip;
Colors colors; Colors colors;
if (_apiController.IsConnected) if (_apiController.IsConnected)
{ {
var pairCount = _pairManager.GetVisibleUserCount(); var pairCount = _pairManager.GetVisibleUserCount();
text = $"\uE044 {pairCount}"; text = $"\uE044 {pairCount}";
if (pairCount > 0) if (pairCount > 0)
{ {
IEnumerable<string> visiblePairs; var preferNote = config.PreferNoteInDtrTooltip;
if (_configService.Current.ShowUidInDtrTooltip) var showUid = config.ShowUidInDtrTooltip;
{
visiblePairs = _pairManager.GetOnlineUserPairs() var visiblePairsQuery = _pairManager.GetOnlineUserPairs()
.Where(x => x.IsVisible) .Where(x => x.IsVisible);
.Select(x => string.Format("{0} ({1})", _configService.Current.PreferNoteInDtrTooltip ? x.GetNote() ?? x.PlayerName : x.PlayerName, x.UserData.AliasOrUID));
} IEnumerable<string> visiblePairs = showUid
else ? 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));
visiblePairs = _pairManager.GetOnlineUserPairs()
.Where(x => x.IsVisible)
.Select(x => string.Format("{0}", _configService.Current.PreferNoteInDtrTooltip ? x.GetNote() ?? x.PlayerName : x.PlayerName));
}
tooltip = $"Lightless Sync: Connected{Environment.NewLine}----------{Environment.NewLine}{string.Join(Environment.NewLine, visiblePairs)}"; tooltip = $"Lightless Sync: Connected{Environment.NewLine}----------{Environment.NewLine}{string.Join(Environment.NewLine, visiblePairs)}";
colors = _configService.Current.DtrColorsPairsInRange; colors = config.DtrColorsPairsInRange;
} }
else else
{ {
tooltip = "Lightless Sync: Connected"; tooltip = "Lightless Sync: Connected";
colors = _configService.Current.DtrColorsDefault; colors = config.DtrColorsDefault;
} }
} }
else else
{ {
text = "\uE044 \uE04C"; text = "\uE044 \uE04C";
tooltip = "Lightless Sync: Not Connected"; tooltip = "Lightless Sync: Not Connected";
colors = _configService.Current.DtrColorsNotConnected; colors = config.DtrColorsNotConnected;
} }
if (!_configService.Current.UseColorsInDtr) if (!config.UseColorsInDtr)
colors = default; 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; _logger.LogInformation("Showing status entry");
_tooltip = tooltip; statusEntry.Shown = true;
_colors = colors;
_entry.Value.Text = BuildColoredSeString(text, colors);
_entry.Value.Tooltip = tooltip;
} }
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 #region Colored SeString
private const byte _colorTypeForeground = 0x13; private const byte _colorTypeForeground = 0x13;
private const byte _colorTypeGlow = 0x14; 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) private static SeString BuildColoredSeString(string text, Colors colors)
{ {
var ssb = new SeStringBuilder(); var ssb = new SeStringBuilder();
if (colors.Foreground != default) AppendColoredSegment(ssb, text, colors);
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));
return ssb.Build(); return ssb.Build();
} }
private static RawPayload BuildColorStartPayload(byte colorType, uint color) 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) private static RawPayload BuildColorEndPayload(byte colorType)
=> new([0x02, colorType, 0x02, 0xEC, 0x03]); => new([0x02, colorType, 0x02, 0xEC, 0x03]);

View File

@@ -0,0 +1,724 @@
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 const float ContentPaddingX = 10f;
private const float ContentPaddingY = 6f;
private const float TitleMessageSpacing = 4f;
private const float ActionButtonSpacing = 8f;
private readonly List<LightlessNotification> _notifications = new();
private readonly object _notificationLock = new();
private readonly LightlessConfigService _configService;
private readonly Dictionary<string, float> _notificationYOffsets = new();
private readonly Dictionary<string, float> _notificationTargetYOffsets = new();
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);
Mediator.Subscribe<ClearAllNotificationsMessage>(this, HandleClearAllNotifications);
}
private void HandleNotificationMessage(LightlessNotificationMessage message) => AddNotification(message.Notification);
private void HandleNotificationDismissMessage(LightlessNotificationDismissMessage message) => RemoveNotification(message.NotificationId);
private void HandleClearAllNotifications(ClearAllNotificationsMessage message) => ClearAllNotifications();
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);
}
}
}
public void ClearAllNotifications()
{
lock (_notificationLock)
{
foreach (var notification in _notifications)
{
StartOutAnimation(notification);
}
}
}
private void StartOutAnimation(LightlessNotification notification)
{
notification.IsAnimatingOut = true;
notification.IsAnimatingIn = false;
}
private bool ShouldRemoveNotification(LightlessNotification notification) =>
notification.IsAnimatingOut && notification.AnimationProgress <= 0.01f;
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();
// Window auto-resizes based on content (AlwaysAutoResize flag)
Position = CalculateWindowPosition(viewport);
PositionCondition = ImGuiCond.Always;
DrawAllNotifications();
}
ImGui.PopStyleVar();
}
private Vector2 CalculateWindowPosition(ImGuiViewportPtr viewport)
{
var corner = _configService.Current.NotificationCorner;
var offsetX = _configService.Current.NotificationOffsetX;
var width = _configService.Current.NotificationWidth;
float posX = corner == NotificationCorner.Left
? viewport.WorkPos.X + offsetX - WindowPaddingOffset
: viewport.WorkPos.X + viewport.WorkSize.X - width - offsetX - WindowPaddingOffset;
return new Vector2(posX, viewport.WorkPos.Y);
}
private void DrawAllNotifications()
{
var offsetY = _configService.Current.NotificationOffsetY;
var startY = ImGui.GetCursorPosY() + offsetY;
for (int i = 0; i < _notifications.Count; i++)
{
var notification = _notifications[i];
if (_notificationYOffsets.TryGetValue(notification.Id, out var yOffset))
{
ImGui.SetCursorPosY(startY + yOffset);
}
DrawNotification(notification, i);
}
}
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)
{
UpdateTargetYPositions();
for (int i = _notifications.Count - 1; i >= 0; i--)
{
var notification = _notifications[i];
UpdateNotificationAnimation(notification, deltaTime);
UpdateNotificationYOffset(notification, deltaTime);
if (ShouldRemoveNotification(notification))
{
_notifications.RemoveAt(i);
_notificationYOffsets.Remove(notification.Id);
_notificationTargetYOffsets.Remove(notification.Id);
}
}
}
private void UpdateTargetYPositions()
{
float currentY = 0f;
for (int i = 0; i < _notifications.Count; i++)
{
var notification = _notifications[i];
if (!_notificationTargetYOffsets.ContainsKey(notification.Id))
{
_notificationTargetYOffsets[notification.Id] = currentY;
_notificationYOffsets[notification.Id] = currentY;
}
else
{
_notificationTargetYOffsets[notification.Id] = currentY;
}
currentY += CalculateNotificationHeight(notification) + _configService.Current.NotificationSpacing;
}
}
private void UpdateNotificationYOffset(LightlessNotification notification, float deltaTime)
{
if (!_notificationYOffsets.ContainsKey(notification.Id) || !_notificationTargetYOffsets.ContainsKey(notification.Id))
return;
var current = _notificationYOffsets[notification.Id];
var target = _notificationTargetYOffsets[notification.Id];
var diff = target - current;
if (Math.Abs(diff) < 0.5f)
{
_notificationYOffsets[notification.Id] = target;
}
else
{
var speed = _configService.Current.NotificationSlideSpeed;
_notificationYOffsets[notification.Id] = current + (diff * deltaTime * speed);
}
}
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 Vector2 CalculateSlideOffset(float alpha)
{
var distance = (1f - alpha) * SlideAnimationDistance;
var corner = _configService.Current.NotificationCorner;
return corner == NotificationCorner.Left ? new Vector2(-distance, 0) : new Vector2(distance, 0);
}
private void DrawNotification(LightlessNotification notification, int index)
{
var alpha = notification.AnimationProgress;
if (alpha <= 0f) return;
var slideOffset = CalculateSlideOffset(alpha);
var originalCursorPos = ImGui.GetCursorPos();
ImGui.SetCursorPos(originalCursorPos + slideOffset);
var notificationHeight = CalculateNotificationHeight(notification);
var notificationWidth = _configService.Current.NotificationWidth;
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) return;
var corner = _configService.Current.NotificationCorner;
Vector2 accentStart, accentEnd;
if (corner == NotificationCorner.Left)
{
accentStart = windowPos + new Vector2(windowSize.X - accentWidth, 0);
accentEnd = windowPos + windowSize;
}
else
{
accentStart = windowPos;
accentEnd = windowPos + new Vector2(accentWidth, windowSize.Y);
}
drawList.AddRectFilled(
accentStart,
accentEnd,
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 contentPos = new Vector2(ContentPaddingX, ContentPaddingY);
var windowSize = ImGui.GetWindowSize();
var contentWidth = CalculateContentWidth(windowSize.X);
ImGui.SetCursorPos(contentPos);
var titleHeight = DrawTitle(notification, contentWidth, alpha);
DrawMessage(notification, contentPos, contentWidth, titleHeight, alpha);
if (HasActions(notification))
{
PositionActionsAtBottom(windowSize.Y);
DrawNotificationActions(notification, contentWidth, alpha);
}
}
private float CalculateContentWidth(float windowWidth) =>
windowWidth - (ContentPaddingX * 2);
private bool HasActions(LightlessNotification notification) =>
notification.Actions.Count > 0;
private void PositionActionsAtBottom(float windowHeight)
{
var actionHeight = ImGui.GetFrameHeight();
var bottomY = windowHeight - ContentPaddingY - actionHeight;
ImGui.SetCursorPosY(bottomY);
ImGui.SetCursorPosX(ContentPaddingX);
}
private float DrawTitle(LightlessNotification notification, float contentWidth, float alpha)
{
var titleColor = new Vector4(1f, 1f, 1f, alpha);
var titleText = FormatTitleText(notification);
using (ImRaii.PushColor(ImGuiCol.Text, titleColor))
{
return DrawWrappedText(titleText, contentWidth);
}
}
private string FormatTitleText(LightlessNotification notification)
{
if (!_configService.Current.ShowNotificationTimestamp)
return notification.Title;
var timestamp = notification.CreatedAt.ToLocalTime().ToString("HH:mm:ss");
return $"[{timestamp}] {notification.Title}";
}
private float DrawWrappedText(string text, float wrapWidth)
{
ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + wrapWidth);
var startY = ImGui.GetCursorPosY();
ImGui.TextWrapped(text);
var height = ImGui.GetCursorPosY() - startY;
ImGui.PopTextWrapPos();
return height;
}
private void DrawMessage(LightlessNotification notification, Vector2 contentPos, float contentWidth, float titleHeight, float alpha)
{
if (string.IsNullOrEmpty(notification.Message)) return;
var messagePos = contentPos + new Vector2(0f, titleHeight + TitleMessageSpacing);
var messageColor = new Vector4(0.9f, 0.9f, 0.9f, alpha);
ImGui.SetCursorPos(messagePos);
using (ImRaii.PushColor(ImGuiCol.Text, messageColor))
{
DrawWrappedText(notification.Message, contentWidth);
}
}
private void DrawNotificationActions(LightlessNotification notification, float availableWidth, float alpha)
{
var buttonWidth = CalculateActionButtonWidth(notification.Actions.Count, availableWidth);
_logger.LogDebug("Drawing {ActionCount} notification actions, buttonWidth: {ButtonWidth}, availableWidth: {AvailableWidth}",
notification.Actions.Count, buttonWidth, availableWidth);
var startX = ImGui.GetCursorPosX();
for (int i = 0; i < notification.Actions.Count; i++)
{
if (i > 0)
{
ImGui.SameLine();
PositionActionButton(i, startX, buttonWidth);
}
DrawActionButton(notification.Actions[i], notification, alpha, buttonWidth);
}
}
private float CalculateActionButtonWidth(int actionCount, float availableWidth)
{
var totalSpacing = (actionCount - 1) * ActionButtonSpacing;
return (availableWidth - totalSpacing) / actionCount;
}
private void PositionActionButton(int index, float startX, float buttonWidth)
{
var xPosition = startX + index * (buttonWidth + ActionButtonSpacing);
ImGui.SetCursorPosX(xPosition);
}
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 = CalculateContentWidth(_configService.Current.NotificationWidth);
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"),
NotificationType.Performance => UIColors.Get("LightlessOrange"),
_ => UIColors.Get("LightlessPurple")
};
}
}

View 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;
}

View 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 (116).
/// </summary>
public static class NotificationSounds
{
// ─────────────────────────────────────────────
// Base <se.#> IDs (116)
// 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

View File

@@ -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 Dalamud.Bindings.ImGui;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Configurations;
using System;
using System.Collections.Generic;
using System.Numerics; 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; _config = config;
public static void Init(LightlessConfigService config) => _config = config; _themeConfig = themeConfig;
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++;
}
} }
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);
} }

View File

@@ -88,7 +88,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
ImGuiHelpers.ScaledDummy(0.5f); ImGuiHelpers.ScaledDummy(0.5f);
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 10.0f); ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 10.0f);
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessYellow2")); ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("PairBlue"));
if (ImGui.Button("Open Lightfinder", new Vector2(200 * ImGuiHelpers.GlobalScale, 0))) if (ImGui.Button("Open Lightfinder", new Vector2(200 * ImGuiHelpers.GlobalScale, 0)))
{ {

View File

@@ -1,4 +1,4 @@
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
@@ -11,12 +11,8 @@ using LightlessSync.Services;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.Utils; using LightlessSync.Utils;
using LightlessSync.WebAPI; using LightlessSync.WebAPI;
using Serilog;
using System;
using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using System.Reflection.Emit;
using System.Threading.Tasks;
namespace LightlessSync.UI; namespace LightlessSync.UI;
@@ -33,13 +29,14 @@ public class TopTabMenu
private bool _pairRequestsExpanded; // useless for now private bool _pairRequestsExpanded; // useless for now
private int _lastRequestCount; private int _lastRequestCount;
private readonly UiSharedService _uiSharedService; private readonly UiSharedService _uiSharedService;
private readonly NotificationService _lightlessNotificationService;
private string _filter = string.Empty; private string _filter = string.Empty;
private int _globalControlCountdown = 0; private int _globalControlCountdown = 0;
private float _pairRequestsHeight = 150f; private float _pairRequestsHeight = 150f;
private string _pairToAdd = string.Empty; private string _pairToAdd = string.Empty;
private SelectedTab _selectedTab = SelectedTab.None; private SelectedTab _selectedTab = SelectedTab.None;
public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService) public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService)
{ {
_lightlessMediator = lightlessMediator; _lightlessMediator = lightlessMediator;
_apiController = apiController; _apiController = apiController;
@@ -47,6 +44,7 @@ public class TopTabMenu
_pairRequestService = pairRequestService; _pairRequestService = pairRequestService;
_dalamudUtilService = dalamudUtilService; _dalamudUtilService = dalamudUtilService;
_uiSharedService = uiSharedService; _uiSharedService = uiSharedService;
_lightlessNotificationService = lightlessNotificationService;
} }
private enum SelectedTab private enum SelectedTab
@@ -198,19 +196,6 @@ public class TopTabMenu
if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f); if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f);
#if DEBUG
if (ImGui.Button("Add Test Pair Request"))
{
var fakeCid = Guid.NewGuid().ToString("N");
var display = _pairRequestService.RegisterIncomingRequest(fakeCid, "Debug pair request");
_lightlessMediator.Publish(new NotificationMessage(
"Pair request received (debug)",
display.Message,
NotificationType.Info,
TimeSpan.FromSeconds(5)));
}
#endif
DrawIncomingPairRequests(availableWidth); DrawIncomingPairRequests(availableWidth);
ImGui.Separator(); ImGui.Separator();
@@ -401,7 +386,7 @@ public class TopTabMenu
try try
{ {
var myCidHash = (await _dalamudUtilService.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); var myCidHash = (await _dalamudUtilService.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
await _apiController.TryPairWithContentId(request.HashedCid, myCidHash).ConfigureAwait(false); await _apiController.TryPairWithContentId(request.HashedCid).ConfigureAwait(false);
_pairRequestService.RemoveRequest(request.HashedCid); _pairRequestService.RemoveRequest(request.HashedCid);
var display = string.IsNullOrEmpty(request.DisplayName) ? request.HashedCid : request.DisplayName; var display = string.IsNullOrEmpty(request.DisplayName) ? request.HashedCid : request.DisplayName;

View File

@@ -11,21 +11,20 @@ namespace LightlessSync.UI
{ "LightlessPurple", "#ad8af5" }, { "LightlessPurple", "#ad8af5" },
{ "LightlessPurpleActive", "#be9eff" }, { "LightlessPurpleActive", "#be9eff" },
{ "LightlessPurpleDefault", "#9375d1" }, { "LightlessPurpleDefault", "#9375d1" },
{ "ButtonDefault", "#323232" }, { "ButtonDefault", "#323232" },
{ "FullBlack", "#000000" }, { "FullBlack", "#000000" },
{ "LightlessBlue", "#a6c2ff" }, { "LightlessBlue", "#a6c2ff" },
{ "LightlessYellow", "#ffe97a" }, { "LightlessYellow", "#ffe97a" },
{ "LightlessYellow2", "#cfbd63" },
{ "LightlessGreen", "#7cd68a" }, { "LightlessGreen", "#7cd68a" },
{ "LightlessOrange", "#ffb366" },
{ "PairBlue", "#88a2db" }, { "PairBlue", "#88a2db" },
{ "DimRed", "#d44444" }, { "DimRed", "#d44444" },
{ "LightlessAdminText", "#ffd663" }, { "LightlessAdminText", "#ffd663" },
{ "LightlessAdminGlow", "#b09343" }, { "LightlessAdminGlow", "#b09343" },
{ "LightlessModeratorText", "#94ffda" }, { "LightlessModeratorText", "#94ffda" },
{ "LightlessModeratorGlow", "#599c84" },
{ "Lightfinder", "#ad8af5" },
{ "LightfinderEdge", "#000000" },
}; };
private static LightlessConfigService? _configService; private static LightlessConfigService? _configService;

View File

@@ -35,16 +35,16 @@ public partial class ApiController
await _lightlessHub!.SendAsync(nameof(UserAddPair), user).ConfigureAwait(false); 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; 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(); 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) 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); return await _lightlessHub!.InvokeAsync<BroadcastStatusBatchDto>(nameof(AreUsersBroadcasting), hashedCids).ConfigureAwait(false);
} }
public async Task<TimeSpan?> GetBroadcastTtl(string hashedCid) public async Task<TimeSpan?> GetBroadcastTtl()
{ {
CheckConnection(); CheckConnection();
return await _lightlessHub!.InvokeAsync<TimeSpan?>(nameof(GetBroadcastTtl), hashedCid).ConfigureAwait(false); return await _lightlessHub!.InvokeAsync<TimeSpan?>(nameof(GetBroadcastTtl)).ConfigureAwait(false);
} }
public async Task UserDelete() public async Task UserDelete()

View File

@@ -1,4 +1,4 @@
using LightlessSync.API.Data; using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Enum;
using LightlessSync.API.Dto; using LightlessSync.API.Dto;
using LightlessSync.API.Dto.CharaData; using LightlessSync.API.Dto.CharaData;
@@ -6,6 +6,7 @@ using LightlessSync.API.Dto.Group;
using LightlessSync.API.Dto.User; using LightlessSync.API.Dto.User;
using LightlessSync.LightlessConfiguration.Models; using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.Utils;
using Microsoft.AspNetCore.SignalR.Client; using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -104,25 +105,20 @@ public partial class ApiController
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task Client_ReceiveBroadcastPairRequest(UserPairNotificationDto dto) public Task Client_ReceiveBroadcastPairRequest(UserPairNotificationDto dto)
{ {
if (dto == null) if (dto == null)
return Task.CompletedTask; return Task.CompletedTask;
var request = _pairRequestService.RegisterIncomingRequest(dto.myHashedCid, dto.message ?? string.Empty); _pairRequestService.RegisterIncomingRequest(dto.myHashedCid, dto.message ?? string.Empty);
Mediator.Publish(new NotificationMessage(
"Pair request received",
request.Message,
NotificationType.Info,
TimeSpan.FromSeconds(5)));
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task Client_UpdateSystemInfo(SystemInfoDto systemInfo) public Task Client_UpdateSystemInfo(SystemInfoDto systemInfo)
{ {
SystemInfoDto = systemInfo; SystemInfoDto = systemInfo;
//Mediator.Publish(new UpdateSystemInfoMessage(systemInfo));
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@@ -1,4 +1,4 @@
using Dalamud.Utility; using Dalamud.Utility;
using LightlessSync.API.Data; using LightlessSync.API.Data;
using LightlessSync.API.Data.Extensions; using LightlessSync.API.Data.Extensions;
using LightlessSync.API.Dto; using LightlessSync.API.Dto;
@@ -32,6 +32,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
private readonly ServerConfigurationManager _serverManager; private readonly ServerConfigurationManager _serverManager;
private readonly TokenProvider _tokenProvider; private readonly TokenProvider _tokenProvider;
private readonly LightlessConfigService _lightlessConfigService; private readonly LightlessConfigService _lightlessConfigService;
private readonly NotificationService _lightlessNotificationService;
private CancellationTokenSource _connectionCancellationTokenSource; private CancellationTokenSource _connectionCancellationTokenSource;
private ConnectionDto? _connectionDto; private ConnectionDto? _connectionDto;
private bool _doNotNotifyOnNextInfo = false; private bool _doNotNotifyOnNextInfo = false;
@@ -44,7 +45,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
public ApiController(ILogger<ApiController> logger, HubFactory hubFactory, DalamudUtilService dalamudUtil, public ApiController(ILogger<ApiController> logger, HubFactory hubFactory, DalamudUtilService dalamudUtil,
PairManager pairManager, PairRequestService pairRequestService, ServerConfigurationManager serverManager, LightlessMediator mediator, PairManager pairManager, PairRequestService pairRequestService, ServerConfigurationManager serverManager, LightlessMediator mediator,
TokenProvider tokenProvider, LightlessConfigService lightlessConfigService) : base(logger, mediator) TokenProvider tokenProvider, LightlessConfigService lightlessConfigService, NotificationService lightlessNotificationService) : base(logger, mediator)
{ {
_hubFactory = hubFactory; _hubFactory = hubFactory;
_dalamudUtil = dalamudUtil; _dalamudUtil = dalamudUtil;
@@ -53,6 +54,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
_serverManager = serverManager; _serverManager = serverManager;
_tokenProvider = tokenProvider; _tokenProvider = tokenProvider;
_lightlessConfigService = lightlessConfigService; _lightlessConfigService = lightlessConfigService;
_lightlessNotificationService = lightlessNotificationService;
_connectionCancellationTokenSource = new CancellationTokenSource(); _connectionCancellationTokenSource = new CancellationTokenSource();
Mediator.Subscribe<DalamudLoginMessage>(this, (_) => DalamudUtilOnLogIn()); Mediator.Subscribe<DalamudLoginMessage>(this, (_) => DalamudUtilOnLogIn());