Compare commits

..

5 Commits

Author SHA1 Message Date
defnotken
2faec601c4 testing a different version extracter
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 34s
2025-10-07 15:36:45 -05:00
defnotken
57b7b8cee9 testing prelease versioning
Some checks failed
Tag and Release Lightless / tag-and-release (push) Failing after 30s
2025-10-07 15:33:52 -05:00
defnotken
b492d12e19 New Dev Build
Some checks failed
Tag and Release Lightless / tag-and-release (push) Failing after 38s
2025-10-07 15:31:33 -05:00
defnotken
6bd59d01aa Merge branch '1.12.1' into dev 2025-10-07 15:30:32 -05:00
defnotken
371b1c2fb1 workflow fix?
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 33s
2025-10-05 18:37:20 -05:00
37 changed files with 871 additions and 4498 deletions

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2025 Lightless-Sync Copyright (c) 2022 Penumbra-Sync
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -16,8 +16,6 @@ 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;
@@ -56,62 +54,6 @@ 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;
@@ -124,25 +66,27 @@ public sealed class FileCacheManager : IHostedService
return NormalizePrefixedPathKey(normalized); return NormalizePrefixedPathKey(normalized);
} }
string? chosenPrefixed = null; var penumbraDir = _ipcManager.Penumbra.ModDirectory;
var chosenLength = -1; if (!string.IsNullOrEmpty(penumbraDir))
if (TryBuildPrefixedPath(normalized, _ipcManager.Penumbra.ModDirectory, PenumbraPrefix, out var penumbraPrefixed, out var penumbraMatch))
{ {
chosenPrefixed = penumbraPrefixed; var normalizedPenumbra = NormalizeSeparators(penumbraDir);
chosenLength = penumbraMatch; var replacement = normalizedPenumbra.EndsWith("\\", StringComparison.Ordinal)
? PenumbraPrefix + "\\"
: PenumbraPrefix;
normalized = normalized.Replace(normalizedPenumbra, replacement, StringComparison.OrdinalIgnoreCase);
} }
if (TryBuildPrefixedPath(normalized, _configService.Current.CacheFolder, CachePrefix, out var cachePrefixed, out var cacheMatch)) var cacheFolder = _configService.Current.CacheFolder;
if (!string.IsNullOrEmpty(cacheFolder))
{ {
if (cacheMatch > chosenLength) var normalizedCache = NormalizeSeparators(cacheFolder);
{ var replacement = normalizedCache.EndsWith("\\", StringComparison.Ordinal)
chosenPrefixed = cachePrefixed; ? CachePrefix + "\\"
chosenLength = cacheMatch; : CachePrefix;
} normalized = normalized.Replace(normalizedCache, replacement, StringComparison.OrdinalIgnoreCase);
} }
return NormalizePrefixedPathKey(chosenPrefixed ?? normalized); return NormalizePrefixedPathKey(normalized);
} }
public FileCacheEntity? CreateCacheEntry(string path) public FileCacheEntity? CreateCacheEntry(string path)
@@ -150,9 +94,7 @@ 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);
var cacheFolder = _configService.Current.CacheFolder; return CreateFileEntity(_configService.Current.CacheFolder.ToLowerInvariant(), CachePrefix, fi);
if (string.IsNullOrEmpty(cacheFolder)) return null;
return CreateFileEntity(cacheFolder, CachePrefix, fi);
} }
public FileCacheEntity? CreateFileEntry(string path) public FileCacheEntity? CreateFileEntry(string path)
@@ -160,18 +102,14 @@ 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);
var modDirectory = _ipcManager.Penumbra.ModDirectory; return CreateFileEntity(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), PenumbraPrefix, fi);
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)
{ {
if (!TryBuildPrefixedPath(fi.FullName, directory, prefix, out var prefixedPath, out _)) var fullName = fi.FullName.ToLowerInvariant();
{ if (!fullName.Contains(directory, StringComparison.Ordinal)) return null;
return null; string prefixedPath = fullName.Replace(directory, prefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal);
}
return CreateFileCacheEntity(fi, prefixedPath); return CreateFileCacheEntity(fi, prefixedPath);
} }
@@ -429,7 +367,6 @@ 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);
@@ -452,53 +389,6 @@ 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
@@ -537,15 +427,7 @@ public sealed class FileCacheManager : IHostedService
AddHashedFile(entity); AddHashedFile(entity);
lock (_fileWriteLock) lock (_fileWriteLock)
{ {
if (!File.Exists(_csvPath)) File.AppendAllLines(_csvPath, new[] { entity.CsvEntry });
{
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));
@@ -664,111 +546,49 @@ 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);
} }
bool rewriteRequired = false; _logger.LogInformation("Found {amount} files in {path}", entries.Length, _csvPath);
bool parseEntries = entries.Length > 0;
int startIndex = 0;
if (entries.Length > 0)
{
var headerLine = entries[0];
var hasHeader = !string.IsNullOrEmpty(headerLine) &&
headerLine.StartsWith(FileCacheVersionHeaderPrefix, StringComparison.OrdinalIgnoreCase);
if (hasHeader)
{
if (!TryParseVersionHeader(headerLine, out var parsedVersion))
{
_logger.LogWarning("Failed to parse file cache version header \"{header}\". Backing up existing cache.", headerLine);
BackupUnsupportedCache("invalid-version");
parseEntries = false;
rewriteRequired = true;
entries = Array.Empty<string>();
}
else if (parsedVersion != FileCacheVersion)
{
_logger.LogWarning("Unsupported file cache version {version} detected (expected {expected}). Backing up existing cache.", parsedVersion, FileCacheVersion);
BackupUnsupportedCache($"v{parsedVersion}");
parseEntries = false;
rewriteRequired = true;
entries = Array.Empty<string>();
}
else
{
startIndex = 1;
}
}
else if (entries.Length > 0)
{
_logger.LogInformation("File cache missing version header, scheduling rewrite.");
rewriteRequired = true;
}
}
var totalEntries = Math.Max(0, entries.Length - startIndex);
Dictionary<string, bool> processedFiles = new(StringComparer.OrdinalIgnoreCase); Dictionary<string, bool> processedFiles = new(StringComparer.OrdinalIgnoreCase);
foreach (var entry in entries)
if (parseEntries && totalEntries > 0)
{ {
_logger.LogInformation("Found {amount} files in {path}", totalEntries, _csvPath); var splittedEntry = entry.Split(CsvSplit, StringSplitOptions.None);
try
for (var index = startIndex; index < entries.Length; index++)
{ {
var entry = entries[index]; var hash = splittedEntry[0];
if (string.IsNullOrWhiteSpace(entry)) 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; continue;
} }
var splittedEntry = entry.Split(CsvSplit, StringSplitOptions.None); processedFiles.Add(path, value: true);
try
long size = -1;
long compressed = -1;
if (splittedEntry.Length > 3)
{ {
var hash = splittedEntry[0]; if (long.TryParse(splittedEntry[3], CultureInfo.InvariantCulture, out long result))
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); size = result;
continue;
} }
if (long.TryParse(splittedEntry[4], CultureInfo.InvariantCulture, out long resultCompressed))
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)) compressed = resultCompressed;
{
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);
} }
AddHashedFile(ReplacePathPrefixes(new FileCacheEntity(hash, path, time, size, compressed)));
} }
catch (Exception ex)
if (processedFiles.Count != totalEntries)
{ {
rewriteRequired = true; _logger.LogWarning(ex, "Failed to initialize entry {entry}, ignoring", entry);
} }
} }
else if (!parseEntries && entries.Length > 0)
{
_logger.LogInformation("Skipping existing file cache entries due to incompatible version.");
}
if (rewriteRequired) if (processedFiles.Count != entries.Length)
{ {
WriteOutFullCsv(); WriteOutFullCsv();
} }

View File

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

View File

@@ -22,13 +22,6 @@ 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;
@@ -63,7 +56,6 @@ 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;
@@ -77,62 +69,17 @@ 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;
// Basic Settings
public float NotificationOpacity { get; set; } = 0.95f;
public int MaxSimultaneousNotifications { get; set; } = 5;
public bool AutoDismissOnAction { get; set; } = true;
public bool DismissNotificationOnClick { get; set; } = false;
public bool ShowNotificationTimestamp { get; set; } = false;
// Position & Layout
public int NotificationOffsetY { get; set; } = 50;
public int NotificationOffsetX { get; set; } = 0;
public float NotificationWidth { get; set; } = 350f;
public float NotificationSpacing { get; set; } = 8f;
// Animation & Effects
public float NotificationAnimationSpeed { get; set; } = 10f;
public float NotificationAccentBarWidth { get; set; } = 3f;
// Duration per Type
public int InfoNotificationDurationSeconds { get; set; } = 10;
public int WarningNotificationDurationSeconds { get; set; } = 15;
public int ErrorNotificationDurationSeconds { get; set; } = 20;
public int PairRequestDurationSeconds { get; set; } = 180;
public int DownloadNotificationDurationSeconds { get; set; } = 300;
public uint CustomInfoSoundId { get; set; } = 2; // Se2
public uint CustomWarningSoundId { get; set; } = 16; // Se15
public uint CustomErrorSoundId { get; set; } = 16; // Se15
public uint PairRequestSoundId { get; set; } = 5; // Se5
public uint DownloadSoundId { get; set; } = 15; // Se14
public bool DisableInfoSound { get; set; } = true;
public bool DisableWarningSound { get; set; } = true;
public bool DisableErrorSound { get; set; } = true;
public bool DisablePairRequestSound { get; set; } = true;
public bool DisableDownloadSound { get; set; } = true;
public bool UseFocusTarget { get; set; } = false; public bool 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

@@ -1,21 +0,0 @@
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

@@ -1,12 +0,0 @@
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,21 +1,16 @@
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
} }

View File

@@ -1,14 +0,0 @@
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.2</Version> <Version>1.12.1-beta.1</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

@@ -114,7 +114,6 @@ 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>();
@@ -143,18 +142,8 @@ 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( collection.AddSingleton((s) => new DtrEntry(s.GetRequiredService<ILogger<DtrEntry>>(), dtrBar, s.GetRequiredService<LightlessConfigService>(),
s.GetRequiredService<ILogger<DtrEntry>>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PairManager>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<ServerConfigurationManager>()));
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>();
@@ -183,14 +172,9 @@ 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( collection.AddSingleton((s) => new NotificationService(s.GetRequiredService<ILogger<NotificationService>>(),
s.GetRequiredService<ILogger<NotificationService>>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<DalamudUtilService>(),
s.GetRequiredService<LightlessConfigService>(), notificationManager, chatGui, s.GetRequiredService<LightlessConfigService>()));
s.GetRequiredService<DalamudUtilService>(),
notificationManager,
chatGui,
s.GetRequiredService<LightlessMediator>(),
s.GetRequiredService<PairRequestService>()));
collection.AddSingleton((s) => collection.AddSingleton((s) =>
{ {
var httpClient = new HttpClient(); var httpClient = new HttpClient();
@@ -198,12 +182,10 @@ 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);
var theme = s.GetRequiredService<UiThemeConfigService>(); LightlessSync.UI.Style.MainStyle.Init(cfg);
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));
@@ -215,7 +197,6 @@ 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>());
@@ -254,12 +235,6 @@ 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>();
@@ -267,9 +242,7 @@ 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<FileDialogManager>(), s.GetRequiredService<LightlessMediator>()));
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,16 +211,6 @@ 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

@@ -7,7 +7,6 @@ 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
@@ -17,11 +16,9 @@ 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; } = false; public bool IsLightFinderAvailable { get; private set; } = true;
public bool IsBroadcasting => _config.Current.BroadcastEnabled; public bool IsBroadcasting => _config.Current.BroadcastEnabled;
private bool _syncedOnStartup = false; private bool _syncedOnStartup = false;
@@ -60,125 +57,24 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
await action().ConfigureAwait(false); await action().ConfigureAwait(false);
} }
private async Task<string?> GetLocalHashedCidAsync(string context) public async Task StartAsync(CancellationToken cancellationToken)
{
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());
IsLightFinderAvailable = false; _apiController.OnConnected += () => _ = CheckLightfinderSupportAsync(cancellationToken);
//_ = 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
@@ -189,54 +85,25 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
if (cancellationToken.IsCancellationRequested) if (cancellationToken.IsCancellationRequested)
return; return;
var hashedCid = await GetLocalHashedCidAsync("Lightfinder state check").ConfigureAwait(false); var dummy = "0".PadLeft(64, '0');
if (string.IsNullOrEmpty(hashedCid))
return;
BroadcastStatusInfoDto? status = null; await _apiController.IsUserBroadcasting(dummy).ConfigureAwait(false);
try await _apiController.SetBroadcastStatus(dummy, true, null).ConfigureAwait(false);
{ await _apiController.GetBroadcastTtl(dummy).ConfigureAwait(false);
status = await _apiController.IsUserBroadcasting(hashedCid).ConfigureAwait(false); await _apiController.AreUsersBroadcasting([dummy]).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;
bool isBroadcasting = status?.IsBroadcasting == true; _config.Current.BroadcastEnabled = false;
TimeSpan? ttl = status?.TTL; _config.Current.BroadcastTtl = DateTime.MinValue;
_config.Save();
if (isBroadcasting) _mediator.Publish(new BroadcastStatusChangedMessage(false, null));
{
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)
{ {
@@ -244,7 +111,14 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
} }
catch (Exception ex) catch (Exception ex)
{ {
HandleLightfinderUnavailable("Lightfinder check failed.", ex); _logger.LogWarning(ex, "Lightfinder check failed.");
IsLightFinderAvailable = false;
_config.Current.BroadcastEnabled = false;
_config.Current.BroadcastTtl = DateTime.MinValue;
_config.Save();
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
} }
} }
@@ -265,38 +139,46 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
}; };
} }
await _apiController.SetBroadcastStatus(msg.Enabled, groupDto).ConfigureAwait(false); await _apiController.SetBroadcastStatus(msg.HashedCid, 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)
{ {
ApplyBroadcastDisabled(forcePublish: true); _config.Current.BroadcastEnabled = false;
_config.Current.BroadcastTtl = DateTime.MinValue;
_config.Save();
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Disabled Lightfinder for Player: {msg.HashedCid}"))); 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;
try TimeSpan? ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false);
{
TimeSpan? ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false);
if (TryApplyBroadcastEnabled(ttl, "client request")) if (ttl is { } remaining && remaining > TimeSpan.Zero)
{
_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.");
}
}
finally
{ {
_waitingForTtlFetch = false; _config.Current.BroadcastTtl = DateTime.UtcNow + remaining;
_config.Current.BroadcastEnabled = true;
_config.Save();
_logger.LogDebug("Fetched TTL from server: {TTL}", remaining);
_mediator.Publish(new BroadcastStatusChangedMessage(true, remaining));
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Enabled Lightfinder for Player: {msg.HashedCid}")));
} }
else
{
_logger.LogWarning("No valid TTL returned after enabling broadcast. Disabling.");
_config.Current.BroadcastEnabled = false;
_config.Current.BroadcastTtl = DateTime.MinValue;
_config.Save();
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
}
_waitingForTtlFetch = false;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -337,24 +219,17 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
return result; return result;
} }
public async Task<TimeSpan?> GetBroadcastTtlAsync(string? cidForLog = null) public async Task<TimeSpan?> GetBroadcastTtlAsync(string cid)
{ {
TimeSpan? ttl = null; TimeSpan? ttl = null;
await RequireConnectionAsync(nameof(GetBroadcastTtlAsync), async () => { await RequireConnectionAsync(nameof(GetBroadcastTtlAsync), async () => {
try try
{ {
ttl = await _apiController.GetBroadcastTtl().ConfigureAwait(false); ttl = await _apiController.GetBroadcastTtl(cid).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {
if (cidForLog is { Length: > 0 }) _logger.LogWarning(ex, "Failed to fetch broadcast TTL for {cid}", cid);
{
_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;
@@ -406,12 +281,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
return; return;
} }
var hashedCid = await GetLocalHashedCidAsync(nameof(ToggleBroadcast)).ConfigureAwait(false); var hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
if (string.IsNullOrEmpty(hashedCid))
{
_logger.LogWarning("ToggleBroadcast - unable to resolve CID.");
return;
}
try try
{ {
@@ -451,31 +321,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
{ {
var hashedCid = await GetLocalHashedCidAsync("startup TTL refresh").ConfigureAwait(false); string hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
if (string.IsNullOrEmpty(hashedCid))
{
_logger.LogDebug("Skipping TTL refresh; hashed CID unavailable.");
return;
}
TimeSpan? ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false); TimeSpan? ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false);
if (TryApplyBroadcastEnabled(ttl, "startup TTL refresh")) if (ttl is { }
remaining && remaining > TimeSpan.Zero)
{ {
_syncedOnStartup = true; _config.Current.BroadcastTtl = DateTime.UtcNow + remaining;
_logger.LogDebug("Refreshed broadcast TTL from server on first OnTick: {TTL}", ttl); _config.Current.BroadcastEnabled = true;
_config.Save();
_logger.LogDebug("Refreshed broadcast TTL from server on first OnTick: {TTL}", remaining);
} }
else else
{ {
_logger.LogWarning("No valid TTL found on OnTick. Disabling broadcast state."); _logger.LogWarning("No valid TTL found on OnTick. Disabling broadcast state.");
ApplyBroadcastDisabled(forcePublish: true); _config.Current.BroadcastEnabled = false;
_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)
@@ -492,7 +362,10 @@ 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.");
ApplyBroadcastDisabled(forcePublish: true); _config.Current.BroadcastEnabled = false;
_config.Current.BroadcastTtl = DateTime.MinValue;
_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 finder - Opens the Lightfinder window" "\t /light lightfinder - 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], "finder", StringComparison.OrdinalIgnoreCase)) else if (string.Equals(splitArgs[0], "lightfinder", 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).ConfigureAwait(false); await _apiController.TryPairWithContentId(receiverCid, senderCid).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(receiverCid)) if (!string.IsNullOrWhiteSpace(receiverCid))
{ {
_pairRequestService.RemoveRequest(receiverCid); _pairRequestService.RemoveRequest(receiverCid);

View File

@@ -17,7 +17,6 @@ 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;
@@ -54,8 +53,6 @@ public record NotificationMessage
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 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;

View File

@@ -1,4 +1,5 @@
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;
@@ -33,6 +34,18 @@ 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,9 +221,6 @@ 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))
@@ -250,17 +247,9 @@ 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()) || _configService.Current.LightfinderLabelShowHidden; bool IsVisible = pNameplateIconNode->AtkResNode.IsVisible() || (pNameplateResNode->IsVisible() && pNameplateTextNode->AtkResNode.IsVisible());
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;
@@ -270,8 +259,8 @@ public unsafe class NameplateHandler : IMediatorSubscriber
continue; continue;
} }
var labelColor = UIColors.Get("Lightfinder"); var labelColor = UIColors.Get("LightlessPurple");
var edgeColor = UIColors.Get("LightfinderEdge"); var edgeColor = UIColors.Get("FullBlack");
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);
@@ -371,35 +360,33 @@ 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,495 +1,62 @@
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 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 PairRequestService _pairRequestService; private readonly LightlessConfigService _configurationService;
private readonly HashSet<string> _shownPairRequestNotifications = new();
public NotificationService( public NotificationService(ILogger<NotificationService> logger, LightlessMediator mediator,
ILogger<NotificationService> logger,
LightlessConfigService configService,
DalamudUtilService dalamudUtilService, DalamudUtilService dalamudUtilService,
INotificationManager notificationManager, INotificationManager notificationManager,
IChatGui chatGui, IChatGui chatGui, LightlessConfigService configurationService) : base(logger, mediator)
LightlessMediator mediator,
PairRequestService pairRequestService) : base(logger, mediator)
{ {
_logger = logger;
_configService = configService;
_dalamudUtilService = dalamudUtilService; _dalamudUtilService = dalamudUtilService;
_notificationManager = notificationManager; _notificationManager = notificationManager;
_chatGui = chatGui; _chatGui = chatGui;
_pairRequestService = pairRequestService; _configurationService = configurationService;
} }
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
{ {
Mediator.Subscribe<NotificationMessage>(this, HandleNotificationMessage); Mediator.Subscribe<NotificationMessage>(this, ShowNotification);
Mediator.Subscribe<PairRequestsUpdatedMessage>(this, HandlePairRequestsUpdated);
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StopAsync(CancellationToken cancellationToken)
public void ShowNotification(string title, string message, NotificationType type = NotificationType.Info,
TimeSpan? duration = null, List<LightlessNotificationAction>? actions = null, uint? soundEffectId = null)
{ {
var notification = CreateNotification(title, message, type, duration, actions, soundEffectId); return Task.CompletedTask;
if (_configService.Current.AutoDismissOnAction && notification.Actions.Any())
{
WrapActionsWithAutoDismiss(notification);
}
if (notification.SoundEffectId.HasValue)
{
PlayNotificationSound(notification.SoundEffectId.Value);
}
Mediator.Publish(new LightlessNotificationMessage(notification));
} }
private LightlessNotification CreateNotification(string title, string message, NotificationType type, private void PrintErrorChat(string? message)
TimeSpan? duration, List<LightlessNotificationAction>? actions, uint? soundEffectId)
{ {
return new LightlessNotification SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Error: " + message);
{ _chatGui.PrintError(se.BuiltString);
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) private void PrintInfoChat(string? message)
{ {
foreach (var action in notification.Actions) SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Info: ").AddItalics(message ?? string.Empty);
{ _chatGui.Print(se.BuiltString);
var originalOnClick = action.OnClick;
action.OnClick = (n) =>
{
originalOnClick(n);
if (_configService.Current.AutoDismissOnAction)
{
DismissNotification(n);
}
};
}
} }
private void DismissNotification(LightlessNotification notification) private void PrintWarnChat(string? message)
{ {
notification.IsDismissed = true; SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ").AddUiForeground("Warning: " + (message ?? string.Empty), 31).AddUiForegroundOff();
notification.IsAnimatingOut = true; _chatGui.Print(se.BuiltString);
} }
public void ShowPairRequestNotification(string senderName, string senderId, Action onAccept, Action onDecline)
{
var notification = new LightlessNotification
{
Id = $"pair_request_{senderId}",
Title = "Pair Request Received",
Message = $"{senderName} wants to directly pair with you.",
Type = NotificationType.PairRequest,
Duration = TimeSpan.FromSeconds(_configService.Current.PairRequestDurationSeconds),
SoundEffectId = GetPairRequestSoundId(),
Actions = CreatePairRequestActions(onAccept, onDecline)
};
if (notification.SoundEffectId.HasValue)
{
PlayNotificationSound(notification.SoundEffectId.Value);
}
Mediator.Publish(new LightlessNotificationMessage(notification));
}
private uint? GetPairRequestSoundId() =>
!_configService.Current.DisablePairRequestSound ? _configService.Current.PairRequestSoundId : null;
private List<LightlessNotificationAction> CreatePairRequestActions(Action onAccept, Action onDecline)
{
return new List<LightlessNotificationAction>
{
new()
{
Id = "accept",
Label = "Accept",
Icon = FontAwesomeIcon.Check,
Color = UIColors.Get("LightlessGreen"),
IsPrimary = true,
OnClick = (n) =>
{
_logger.LogInformation("Pair request accepted");
onAccept();
DismissNotification(n);
}
},
new()
{
Id = "decline",
Label = "Decline",
Icon = FontAwesomeIcon.Times,
Color = UIColors.Get("DimRed"),
IsDestructive = true,
OnClick = (n) =>
{
_logger.LogInformation("Pair request declined");
onDecline();
DismissNotification(n);
}
}
};
}
public void ShowDownloadCompleteNotification(string fileName, int fileCount, Action? onOpenFolder = null)
{
var notification = new LightlessNotification
{
Title = "Download Complete",
Message = FormatDownloadCompleteMessage(fileName, fileCount),
Type = NotificationType.Info,
Duration = TimeSpan.FromSeconds(8),
Actions = CreateDownloadCompleteActions(onOpenFolder),
SoundEffectId = NotificationSounds.DownloadComplete
};
if (notification.SoundEffectId.HasValue)
{
PlayNotificationSound(notification.SoundEffectId.Value);
}
Mediator.Publish(new LightlessNotificationMessage(notification));
}
private string FormatDownloadCompleteMessage(string fileName, int fileCount) =>
fileCount > 1
? $"Downloaded {fileCount} files successfully."
: $"Downloaded {fileName} successfully.";
private List<LightlessNotificationAction> CreateDownloadCompleteActions(Action? onOpenFolder)
{
var actions = new List<LightlessNotificationAction>();
if (onOpenFolder != null)
{
actions.Add(new LightlessNotificationAction
{
Id = "open_folder",
Label = "Open Folder",
Icon = FontAwesomeIcon.FolderOpen,
Color = UIColors.Get("LightlessBlue"),
OnClick = (n) =>
{
onOpenFolder();
DismissNotification(n);
}
});
}
return actions;
}
public void ShowErrorNotification(string title, string message, Exception? exception = null, Action? onRetry = null,
Action? onViewLog = null)
{
var notification = new LightlessNotification
{
Title = title,
Message = FormatErrorMessage(message, exception),
Type = NotificationType.Error,
Duration = TimeSpan.FromSeconds(15),
Actions = CreateErrorActions(onRetry, onViewLog),
SoundEffectId = NotificationSounds.Error
};
if (notification.SoundEffectId.HasValue)
{
PlayNotificationSound(notification.SoundEffectId.Value);
}
Mediator.Publish(new LightlessNotificationMessage(notification));
}
private string FormatErrorMessage(string message, Exception? exception) =>
exception != null ? $"{message}\n\nError: {exception.Message}" : message;
private List<LightlessNotificationAction> CreateErrorActions(Action? onRetry, Action? onViewLog)
{
var actions = new List<LightlessNotificationAction>();
if (onRetry != null)
{
actions.Add(new LightlessNotificationAction
{
Id = "retry",
Label = "Retry",
Icon = FontAwesomeIcon.Redo,
Color = UIColors.Get("LightlessBlue"),
OnClick = (n) =>
{
onRetry();
DismissNotification(n);
}
});
}
if (onViewLog != null)
{
actions.Add(new LightlessNotificationAction
{
Id = "view_log",
Label = "View Log",
Icon = FontAwesomeIcon.FileAlt,
Color = UIColors.Get("LightlessYellow"),
OnClick = (n) => onViewLog()
});
}
return actions;
}
public void ShowPairDownloadNotification(List<(string playerName, float progress, string status)> downloadStatus,
int queueWaiting = 0)
{
var userDownloads = downloadStatus.Where(x => x.playerName != "Pair Queue").ToList();
var totalProgress = userDownloads.Count > 0 ? userDownloads.Average(x => x.progress) : 0f;
var message = BuildPairDownloadMessage(userDownloads, queueWaiting);
var notification = new LightlessNotification
{
Id = "pair_download_progress",
Title = "Downloading Pair Data",
Message = message,
Type = NotificationType.Download,
Duration = TimeSpan.FromSeconds(_configService.Current.DownloadNotificationDurationSeconds),
ShowProgress = true,
Progress = totalProgress
};
Mediator.Publish(new LightlessNotificationMessage(notification));
if (AreAllDownloadsCompleted(userDownloads))
{
DismissPairDownloadNotification();
}
}
private string BuildPairDownloadMessage(List<(string playerName, float progress, string status)> userDownloads,
int queueWaiting)
{
var messageParts = new List<string>();
if (queueWaiting > 0)
{
messageParts.Add($"Queue: {queueWaiting} waiting");
}
if (userDownloads.Count > 0)
{
var completedCount = userDownloads.Count(x => x.progress >= 1.0f);
messageParts.Add($"Progress: {completedCount}/{userDownloads.Count} completed");
}
var activeDownloadLines = BuildActiveDownloadLines(userDownloads);
if (!string.IsNullOrEmpty(activeDownloadLines))
{
messageParts.Add(activeDownloadLines);
}
return string.Join("\n", messageParts);
}
private string BuildActiveDownloadLines(List<(string playerName, float progress, string status)> userDownloads)
{
var activeDownloads = userDownloads
.Where(x => x.progress < 1.0f)
.Take(_configService.Current.MaxConcurrentPairApplications);
if (!activeDownloads.Any()) return string.Empty;
return string.Join("\n", activeDownloads.Select(x => $"• {x.playerName}: {FormatDownloadStatus(x)}"));
}
private string FormatDownloadStatus((string playerName, float progress, string status) download) =>
download.status switch
{
"downloading" => $"{download.progress:P0}",
"decompressing" => "decompressing",
"queued" => "queued",
"waiting" => "waiting for slot",
_ => download.status
};
private bool AreAllDownloadsCompleted(List<(string playerName, float progress, string status)> userDownloads) =>
userDownloads.Any() && userDownloads.All(x => x.progress >= 1.0f);
public void DismissPairDownloadNotification() =>
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
private TimeSpan GetDefaultDurationForType(NotificationType type) => type switch
{
NotificationType.Info => TimeSpan.FromSeconds(_configService.Current.InfoNotificationDurationSeconds),
NotificationType.Warning => TimeSpan.FromSeconds(_configService.Current.WarningNotificationDurationSeconds),
NotificationType.Error => TimeSpan.FromSeconds(_configService.Current.ErrorNotificationDurationSeconds),
NotificationType.PairRequest => TimeSpan.FromSeconds(_configService.Current.PairRequestDurationSeconds),
NotificationType.Download => TimeSpan.FromSeconds(_configService.Current.DownloadNotificationDurationSeconds),
_ => TimeSpan.FromSeconds(10)
};
private uint? GetSoundEffectId(NotificationType type, uint? overrideSoundId)
{
if (overrideSoundId.HasValue) return overrideSoundId;
if (IsSoundDisabledForType(type)) return null;
return GetConfiguredSoundForType(type);
}
private bool IsSoundDisabledForType(NotificationType type) => type switch
{
NotificationType.Info => _configService.Current.DisableInfoSound,
NotificationType.Warning => _configService.Current.DisableWarningSound,
NotificationType.Error => _configService.Current.DisableErrorSound,
NotificationType.Download => _configService.Current.DisableDownloadSound,
_ => false
};
private uint GetConfiguredSoundForType(NotificationType type) => type switch
{
NotificationType.Info => _configService.Current.CustomInfoSoundId,
NotificationType.Warning => _configService.Current.CustomWarningSoundId,
NotificationType.Error => _configService.Current.CustomErrorSoundId,
NotificationType.Download => _configService.Current.DownloadSoundId,
_ => NotificationSounds.GetDefaultSound(type)
};
private void PlayNotificationSound(uint soundEffectId)
{
try
{
UIGlobals.PlayChatSoundEffect(soundEffectId);
_logger.LogDebug("Played notification sound effect {SoundId} via ChatGui", soundEffectId);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to play notification sound effect {SoundId}", soundEffectId);
}
}
private void HandleNotificationMessage(NotificationMessage msg)
{
_logger.LogInformation("{msg}", msg.ToString());
if (!_dalamudUtilService.IsLoggedIn) return;
var location = GetNotificationLocation(msg.Type);
ShowNotificationLocationBased(msg, location);
}
private NotificationLocation GetNotificationLocation(NotificationType type) =>
_configService.Current.UseLightlessNotifications
? GetLightlessNotificationLocation(type)
: GetClassicNotificationLocation(type);
private NotificationLocation GetLightlessNotificationLocation(NotificationType type) => type switch
{
NotificationType.Info => _configService.Current.LightlessInfoNotification,
NotificationType.Warning => _configService.Current.LightlessWarningNotification,
NotificationType.Error => _configService.Current.LightlessErrorNotification,
NotificationType.PairRequest => _configService.Current.LightlessPairRequestNotification,
NotificationType.Download => _configService.Current.LightlessDownloadNotification,
_ => NotificationLocation.LightlessUi
};
private NotificationLocation GetClassicNotificationLocation(NotificationType type) => type switch
{
NotificationType.Info => _configService.Current.InfoNotification,
NotificationType.Warning => _configService.Current.WarningNotification,
NotificationType.Error => _configService.Current.ErrorNotification,
NotificationType.PairRequest => NotificationLocation.Toast,
NotificationType.Download => NotificationLocation.Toast,
_ => NotificationLocation.Nowhere
};
private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location)
{
switch (location)
{
case NotificationLocation.Toast:
ShowToast(msg);
break;
case NotificationLocation.Chat:
ShowChat(msg);
break;
case NotificationLocation.Both:
ShowToast(msg);
ShowChat(msg);
break;
case NotificationLocation.LightlessUi:
ShowLightlessNotification(msg);
break;
case NotificationLocation.ChatAndLightlessUi:
ShowChat(msg);
ShowLightlessNotification(msg);
break;
case NotificationLocation.Nowhere:
break;
}
}
private void ShowLightlessNotification(NotificationMessage msg)
{
var duration = msg.TimeShownOnScreen ?? GetDefaultDurationForType(msg.Type);
ShowNotification(msg.Title ?? "Lightless Sync", msg.Message ?? string.Empty, msg.Type, duration, null, null);
}
private void ShowToast(NotificationMessage msg)
{
var dalamudType = ConvertToDalamudNotificationType(msg.Type);
_notificationManager.AddNotification(new Notification()
{
Content = msg.Message ?? string.Empty,
Title = msg.Title,
Type = dalamudType,
Minimized = false,
InitialDuration = msg.TimeShownOnScreen ?? TimeSpan.FromSeconds(3)
});
}
private Dalamud.Interface.ImGuiNotification.NotificationType
ConvertToDalamudNotificationType(NotificationType type) => type switch
{
NotificationType.Error => Dalamud.Interface.ImGuiNotification.NotificationType.Error,
NotificationType.Warning => Dalamud.Interface.ImGuiNotification.NotificationType.Warning,
_ => Dalamud.Interface.ImGuiNotification.NotificationType.Info
};
private void ShowChat(NotificationMessage msg) private void ShowChat(NotificationMessage msg)
{ {
switch (msg.Type) switch (msg.Type)
@@ -508,54 +75,67 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
} }
} }
private void PrintErrorChat(string? message) private void ShowNotification(NotificationMessage msg)
{ {
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Error: " + message); Logger.LogInformation("{msg}", msg.ToString());
_chatGui.PrintError(se.BuiltString);
}
private void PrintInfoChat(string? message) if (!_dalamudUtilService.IsLoggedIn) return;
{
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Info: ")
.AddItalics(message ?? string.Empty);
_chatGui.Print(se.BuiltString);
}
private void PrintWarnChat(string? message) switch (msg.Type)
{
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ")
.AddUiForeground("Warning: " + (message ?? string.Empty), 31).AddUiForegroundOff();
_chatGui.Print(se.BuiltString);
}
private void HandlePairRequestsUpdated(PairRequestsUpdatedMessage _)
{
var activeRequests = _pairRequestService.GetActiveRequests();
var activeRequestIds = activeRequests.Select(r => r.HashedCid).ToHashSet();
// Dismiss notifications for requests that are no longer active
var notificationsToRemove = _shownPairRequestNotifications
.Where(hashedCid => !activeRequestIds.Contains(hashedCid))
.ToList();
foreach (var hashedCid in notificationsToRemove)
{ {
var notificationId = $"pair_request_{hashedCid}"; case NotificationType.Info:
Mediator.Publish(new LightlessNotificationDismissMessage(notificationId)); ShowNotificationLocationBased(msg, _configurationService.Current.InfoNotification);
_shownPairRequestNotifications.Remove(hashedCid); break;
}
// Show/update notifications for all active requests case NotificationType.Warning:
foreach (var request in activeRequests) ShowNotificationLocationBased(msg, _configurationService.Current.WarningNotification);
{ break;
_shownPairRequestNotifications.Add(request.HashedCid);
ShowPairRequestNotification( case NotificationType.Error:
request.DisplayName, ShowNotificationLocationBased(msg, _configurationService.Current.ErrorNotification);
request.HashedCid, break;
() => _pairRequestService.AcceptPairRequest(request.HashedCid, request.DisplayName),
() => _pairRequestService.DeclinePairRequest(request.HashedCid)
);
} }
} }
private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location)
{
switch (location)
{
case NotificationLocation.Toast:
ShowToast(msg);
break;
case NotificationLocation.Chat:
ShowChat(msg);
break;
case NotificationLocation.Both:
ShowToast(msg);
ShowChat(msg);
break;
case NotificationLocation.Nowhere:
break;
}
}
private void ShowToast(NotificationMessage msg)
{
Dalamud.Interface.ImGuiNotification.NotificationType dalamudType = msg.Type switch
{
NotificationType.Error => Dalamud.Interface.ImGuiNotification.NotificationType.Error,
NotificationType.Warning => Dalamud.Interface.ImGuiNotification.NotificationType.Warning,
NotificationType.Info => Dalamud.Interface.ImGuiNotification.NotificationType.Info,
_ => Dalamud.Interface.ImGuiNotification.NotificationType.Info
};
_notificationManager.AddNotification(new Notification()
{
Content = msg.Message ?? string.Empty,
Title = msg.Title,
Type = dalamudType,
Minimized = false,
InitialDuration = msg.TimeShownOnScreen ?? TimeSpan.FromSeconds(3)
});
}
} }

View File

@@ -1,8 +1,6 @@
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;
@@ -13,18 +11,16 @@ 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, Lazy<WebAPI.ApiController> apiController) public PairRequestService(ILogger<PairRequestService> logger, LightlessMediator mediator, DalamudUtilService dalamudUtil, PairManager pairManager)
: base(logger, mediator) : base(logger, mediator)
{ {
_dalamudUtil = dalamudUtil; _dalamudUtil = dalamudUtil;
_pairManager = pairManager; _pairManager = pairManager;
_apiController = apiController;
Mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, _ => Mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, _ =>
{ {
@@ -187,40 +183,6 @@ 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

@@ -504,7 +504,7 @@ public class ServerConfigurationManager
internal void RenameTag(Dictionary<string, List<string>> tags, HashSet<string> storage, string oldName, string newName) internal void RenameTag(Dictionary<string, List<string>> tags, HashSet<string> storage, string oldName, string newName)
{ {
if (newName.Length < _maxCharactersFolder) if (newName.Length > _maxCharactersFolder)
{ {
storage.Remove(oldName); storage.Remove(oldName);
storage.Add(newName); storage.Add(newName);
@@ -607,9 +607,8 @@ 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));
using var request = new HttpRequestMessage(HttpMethod.Get, oauthCheckUri); _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); var response = await _httpClient.GetAsync(oauthCheckUri).ConfigureAwait(false);
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,10 +1,9 @@
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;
@@ -23,8 +22,7 @@ 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, LightlessMediator lightlessMediator) : base(logger, lightlessMediator)
NotificationService notificationService) : base(logger, lightlessMediator)
{ {
_logger = logger; _logger = logger;
_logger.LogTrace("Creating {type}", GetType().Name); _logger.LogTrace("Creating {type}", GetType().Name);
@@ -121,15 +119,7 @@ public sealed class UiService : DisposableMediatorSubscriberBase
private void Draw() private void Draw()
{ {
MainStyle.PushStyle(); _windowSystem.Draw();
try _fileDialogManager.Draw();
{
_windowSystem.Draw();
_fileDialogManager.Draw();
}
finally
{
MainStyle.PopStyle();
}
} }
} }

View File

@@ -1,5 +1,4 @@
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;
@@ -210,7 +209,7 @@ namespace LightlessSync.UI
else else
{ {
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
ImGui.Text("The Lightfinders light wanes, but not in vain."); // cringe.. ImGui.Text("The Lightfinder<EFBFBD>s light wanes, but not in vain."); // cringe..
ImGui.PopStyleColor(); ImGui.PopStyleColor();
} }
} }
@@ -253,27 +252,12 @@ 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, NotificationService lightlessNotificationService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService) PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService) : 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, lightlessNotificationService); _tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService, dalamudUtilService);
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,12 +22,9 @@ 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;
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, PairProcessingLimiter pairProcessingLimiter, FileUploadManager fileTransferManager, LightlessMediator mediator, UiSharedService uiShared, PerformanceCollectorService performanceCollectorService)
PerformanceCollectorService performanceCollectorService, NotificationService notificationService)
: base(logger, mediator, "Lightless Sync Downloads", performanceCollectorService) : base(logger, mediator, "Lightless Sync Downloads", performanceCollectorService)
{ {
_dalamudUtilService = dalamudUtilService; _dalamudUtilService = dalamudUtilService;
@@ -35,7 +32,6 @@ public class DownloadUi : WindowMediatorSubscriberBase
_pairProcessingLimiter = pairProcessingLimiter; _pairProcessingLimiter = pairProcessingLimiter;
_fileTransferManager = fileTransferManager; _fileTransferManager = fileTransferManager;
_uiShared = uiShared; _uiShared = uiShared;
_notificationService = notificationService;
SizeConstraints = new WindowSizeConstraints() SizeConstraints = new WindowSizeConstraints()
{ {
@@ -60,14 +56,7 @@ 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) => Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _));
{
_currentDownloads.TryRemove(msg.DownloadId, out _);
if (!_currentDownloads.Any())
{
_notificationService.DismissPairDownloadNotification();
}
});
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) =>
@@ -88,7 +77,19 @@ 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)
@@ -121,64 +122,28 @@ public class DownloadUi : WindowMediatorSubscriberBase
try try
{ {
// Check if download notifications are enabled (not set to TextOverlay) foreach (var item in _currentDownloads.ToList())
var useNotifications = _configService.Current.UseLightlessNotifications
? _configService.Current.LightlessDownloadNotification != NotificationLocation.TextOverlay
: _configService.Current.UseNotificationsForDownloads;
if (useNotifications)
{ {
// Use notification system var dlSlot = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForSlot);
if (_currentDownloads.Any()) var dlQueue = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForQueue);
{ var dlProg = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Downloading);
UpdateDownloadNotification(limiterSnapshot); var dlDecomp = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Decompressing);
_notificationDismissed = false; var totalFiles = item.Value.Sum(c => c.Value.TotalFiles);
} var transferredFiles = item.Value.Sum(c => c.Value.TransferredFiles);
else if (!_notificationDismissed) var totalBytes = item.Value.Sum(c => c.Value.TotalBytes);
{ var transferredBytes = item.Value.Sum(c => c.Value.TransferredBytes);
_notificationService.DismissPairDownloadNotification();
_notificationDismissed = true;
}
}
else
{
if (limiterSnapshot.IsEnabled)
{
var queueColor = limiterSnapshot.Waiting > 0 ? ImGuiColors.DalamudYellow : ImGuiColors.DalamudGrey;
var queueText = $"Pair queue {limiterSnapshot.InFlight}/{limiterSnapshot.Limit}";
queueText += limiterSnapshot.Waiting > 0 ? $" ({limiterSnapshot.Waiting} waiting, {limiterSnapshot.Remaining} free)" : $" ({limiterSnapshot.Remaining} free)";
UiSharedService.DrawOutlinedFont(queueText, queueColor, new Vector4(0, 0, 0, 255), 1);
ImGui.NewLine();
}
else
{
UiSharedService.DrawOutlinedFont("Pair apply limiter disabled", ImGuiColors.DalamudGrey, new Vector4(0, 0, 0, 255), 1);
ImGui.NewLine();
}
foreach (var item in _currentDownloads.ToList()) UiSharedService.DrawOutlinedFont($"▼", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
{ ImGui.SameLine();
var dlSlot = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForSlot); var xDistance = ImGui.GetCursorPosX();
var dlQueue = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForQueue); UiSharedService.DrawOutlinedFont(
var dlProg = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Downloading); $"{item.Key.Name} [W:{dlSlot}/Q:{dlQueue}/P:{dlProg}/D:{dlDecomp}]",
var dlDecomp = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Decompressing); ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
var totalFiles = item.Value.Sum(c => c.Value.TotalFiles); ImGui.NewLine();
var transferredFiles = item.Value.Sum(c => c.Value.TransferredFiles); ImGui.SameLine(xDistance);
var totalBytes = item.Value.Sum(c => c.Value.TotalBytes); UiSharedService.DrawOutlinedFont(
var transferredBytes = item.Value.Sum(c => c.Value.TransferredBytes); $"{transferredFiles}/{totalFiles} ({UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)})",
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
UiSharedService.DrawOutlinedFont($"▼", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
ImGui.SameLine();
var xDistance = ImGui.GetCursorPosX();
UiSharedService.DrawOutlinedFont(
$"{item.Key.Name} [W:{dlSlot}/Q:{dlQueue}/P:{dlProg}/D:{dlDecomp}]",
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
ImGui.NewLine();
ImGui.SameLine(xDistance);
UiSharedService.DrawOutlinedFont(
$"{transferredFiles}/{totalFiles} ({UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)})",
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
}
} }
} }
catch catch
@@ -297,40 +262,4 @@ public class DownloadUi : WindowMediatorSubscriberBase
MaximumSize = new Vector2(300, maxHeight), MaximumSize = new Vector2(300, maxHeight),
}; };
} }
private void UpdateDownloadNotification(PairProcessingLimiterSnapshot limiterSnapshot)
{
var downloadStatus = new List<(string playerName, float progress, string status)>();
foreach (var item in _currentDownloads.ToList())
{
var dlSlot = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForSlot);
var dlQueue = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForQueue);
var dlProg = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Downloading);
var dlDecomp = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Decompressing);
var totalFiles = item.Value.Sum(c => c.Value.TotalFiles);
var transferredFiles = item.Value.Sum(c => c.Value.TransferredFiles);
var totalBytes = item.Value.Sum(c => c.Value.TotalBytes);
var transferredBytes = item.Value.Sum(c => c.Value.TransferredBytes);
var progress = totalBytes > 0 ? (float)transferredBytes / totalBytes : 0f;
string status;
if (dlDecomp > 0) status = "decompressing";
else if (dlProg > 0) status = "downloading";
else if (dlQueue > 0) status = "queued";
else if (dlSlot > 0) status = "waiting";
else status = "completed";
downloadStatus.Add((item.Key.Name, progress, status));
}
// Pass queue waiting count separately, show notification if there are downloads or queue items
var queueWaiting = limiterSnapshot.IsEnabled ? limiterSnapshot.Waiting : 0;
if (downloadStatus.Any() || queueWaiting > 0)
{
_notificationService.ShowPairDownloadNotification(downloadStatus, queueWaiting);
}
}
} }

View File

@@ -1,92 +1,56 @@
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> _statusEntry; private readonly Lazy<IDtrBarEntry> _entry;
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? _statusText; private string? _text;
private string? _statusTooltip; private string? _tooltip;
private Colors _statusColors; private Colors _colors;
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( public DtrEntry(ILogger<DtrEntry> logger, IDtrBar dtrBar, ConfigurationServiceBase<LightlessConfig> configService, LightlessMediator lightlessMediator, PairManager pairManager, ApiController apiController, ServerConfigurationManager serverManager)
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;
_statusEntry = new(CreateStatusEntry); _entry = new(CreateEntry);
_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 (_statusEntry.IsValueCreated) if (_entry.IsValueCreated)
{ {
_logger.LogDebug("Disposing DtrEntry"); _logger.LogDebug("Disposing DtrEntry");
Clear(); Clear();
_statusEntry.Value.Remove(); _entry.Value.Remove();
} }
if (_lightfinderEntry.IsValueCreated)
_lightfinderEntry.Value.Remove();
} }
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
@@ -106,7 +70,7 @@ public sealed class DtrEntry : IDisposable, IHostedService
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
// ignore cancelled
} }
finally finally
{ {
@@ -116,66 +80,33 @@ public sealed class DtrEntry : IDisposable, IHostedService
private void Clear() private void Clear()
{ {
HideStatusEntry(); if (!_entry.IsValueCreated) return;
HideLightfinderEntry(); _logger.LogInformation("Clearing entry");
_text = null;
_tooltip = null;
_colors = default;
_entry.Value.Shown = false;
} }
private void HideStatusEntry() private IDtrBarEntry CreateEntry()
{ {
if (_statusEntry.IsValueCreated && _statusEntry.Value.Shown) _logger.LogTrace("Creating new DtrBar entry");
{
_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 => OnStatusEntryClick(interactionEvent); entry.OnClick = interactionEvent => OnClickEvent(interactionEvent);
return entry; return entry;
} }
private IDtrBarEntry CreateLightfinderEntry() private void OnClickEvent(DtrInteractionEvent interactionEvent)
{ {
_logger.LogTrace("Creating Lightfinder DtrBar entry"); if (interactionEvent.ClickType.Equals(MouseClickType.Left) && !interactionEvent.ModifierKeys.Equals(ClickModifierKeys.Shift))
var entry = _dtrBar.Get("Lightfinder");
entry.OnClick = interactionEvent => OnLightfinderEntryClick(interactionEvent);
return entry;
}
private void OnStatusEntryClick(DtrInteractionEvent interactionEvent)
{
if (interactionEvent.ClickType.Equals(MouseClickType.Left))
{ {
if (interactionEvent.ModifierKeys.HasFlag(ClickModifierKeys.Shift)) _lightlessMediator.Publish(new UiToggleMessage(typeof(CompactUi)));
{ }
_lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi))); else if (interactionEvent.ClickType.Equals(MouseClickType.Left) && interactionEvent.ModifierKeys.Equals(ClickModifierKeys.Shift))
} {
else _lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(CompactUi)));
}
return;
} }
if (interactionEvent.ClickType.Equals(MouseClickType.Right)) if (interactionEvent.ClickType.Equals(MouseClickType.Right))
@@ -200,17 +131,6 @@ 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)
@@ -223,278 +143,96 @@ public sealed class DtrEntry : IDisposable, IHostedService
private void Update() private void Update()
{ {
var config = _configService.Current; if (!_configService.Current.EnableDtrEntry || !_configService.Current.HasValidSetup())
if (!config.HasValidSetup())
{ {
HideStatusEntry(); if (_entry.IsValueCreated && _entry.Value.Shown)
HideLightfinderEntry(); {
_logger.LogInformation("Disabling entry");
Clear();
}
return; return;
} }
if (config.EnableDtrEntry) if (!_entry.Value.Shown)
UpdateStatusEntry(config); {
else _logger.LogInformation("Showing entry");
HideStatusEntry(); _entry.Value.Shown = true;
}
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)
{ {
var preferNote = config.PreferNoteInDtrTooltip; IEnumerable<string> visiblePairs;
var showUid = config.ShowUidInDtrTooltip; if (_configService.Current.ShowUidInDtrTooltip)
{
var visiblePairsQuery = _pairManager.GetOnlineUserPairs() visiblePairs = _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 }
? visiblePairsQuery.Select(x => string.Format("{0} ({1})", preferNote ? x.GetNote() ?? x.PlayerName : x.PlayerName, x.UserData.AliasOrUID)) else
: 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 = config.DtrColorsPairsInRange; colors = _configService.Current.DtrColorsPairsInRange;
} }
else else
{ {
tooltip = "Lightless Sync: Connected"; tooltip = "Lightless Sync: Connected";
colors = config.DtrColorsDefault; colors = _configService.Current.DtrColorsDefault;
} }
} }
else else
{ {
text = "\uE044 \uE04C"; text = "\uE044 \uE04C";
tooltip = "Lightless Sync: Not Connected"; tooltip = "Lightless Sync: Not Connected";
colors = config.DtrColorsNotConnected; colors = _configService.Current.DtrColorsNotConnected;
} }
if (!config.UseColorsInDtr) if (!_configService.Current.UseColorsInDtr)
colors = default; colors = default;
var statusEntry = _statusEntry.Value; if (!string.Equals(text, _text, StringComparison.Ordinal) || !string.Equals(tooltip, _tooltip, StringComparison.Ordinal) || colors != _colors)
if (!statusEntry.Shown)
{ {
_logger.LogInformation("Showing status entry"); _text = text;
statusEntry.Shown = true; _tooltip = tooltip;
_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();
AppendColoredSegment(ssb, text, colors); if (colors.Foreground != default)
ssb.Add(BuildColorStartPayload(_colorTypeForeground, colors.Foreground));
if (colors.Glow != default)
ssb.Add(BuildColorStartPayload(_colorTypeGlow, colors.Glow));
ssb.AddText(text);
if (colors.Glow != default)
ssb.Add(BuildColorEndPayload(_colorTypeGlow));
if (colors.Foreground != default)
ssb.Add(BuildColorEndPayload(_colorTypeForeground));
return ssb.Build(); return ssb.Build();
} }
private static RawPayload BuildColorStartPayload(byte colorType, uint color) private static RawPayload BuildColorStartPayload(byte colorType, uint color)
=> new(unchecked([ => new(unchecked([0x02, colorType, 0x05, 0xF6, byte.Max((byte)color, 0x01), byte.Max((byte)(color >> 8), 0x01), byte.Max((byte)(color >> 16), 0x01), 0x03]));
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

@@ -1,596 +0,0 @@
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.UI.Models;
using Microsoft.Extensions.Logging;
using System.Numerics;
using Dalamud.Bindings.ImGui;
namespace LightlessSync.UI;
public class LightlessNotificationUI : WindowMediatorSubscriberBase
{
private const float NotificationMinHeight = 60f;
private const float NotificationMaxHeight = 250f;
private const float WindowPaddingOffset = 6f;
private const float SlideAnimationDistance = 100f;
private const float OutAnimationSpeedMultiplier = 0.7f;
private readonly List<LightlessNotification> _notifications = new();
private readonly object _notificationLock = new();
private readonly LightlessConfigService _configService;
public LightlessNotificationUI(ILogger<LightlessNotificationUI> logger, LightlessMediator mediator, PerformanceCollectorService performanceCollector, LightlessConfigService configService)
: base(logger, mediator, "Lightless Notifications##LightlessNotifications", performanceCollector)
{
_configService = configService;
Flags = ImGuiWindowFlags.NoDecoration |
ImGuiWindowFlags.NoMove |
ImGuiWindowFlags.NoResize |
ImGuiWindowFlags.NoSavedSettings |
ImGuiWindowFlags.NoFocusOnAppearing |
ImGuiWindowFlags.NoNav |
ImGuiWindowFlags.NoBackground |
ImGuiWindowFlags.NoCollapse |
ImGuiWindowFlags.AlwaysAutoResize;
PositionCondition = ImGuiCond.Always;
SizeCondition = ImGuiCond.FirstUseEver;
IsOpen = false;
RespectCloseHotkey = false;
DisableWindowSounds = true;
Mediator.Subscribe<LightlessNotificationMessage>(this, HandleNotificationMessage);
Mediator.Subscribe<LightlessNotificationDismissMessage>(this, HandleNotificationDismissMessage);
}
private void HandleNotificationMessage(LightlessNotificationMessage message) =>
AddNotification(message.Notification);
private void HandleNotificationDismissMessage(LightlessNotificationDismissMessage message) =>
RemoveNotification(message.NotificationId);
public void AddNotification(LightlessNotification notification)
{
lock (_notificationLock)
{
var existingNotification = _notifications.FirstOrDefault(n => n.Id == notification.Id);
if (existingNotification != null)
{
UpdateExistingNotification(existingNotification, notification);
}
else
{
_notifications.Add(notification);
_logger.LogDebug("Added new notification: {Title}", notification.Title);
}
if (!IsOpen) IsOpen = true;
}
}
private void UpdateExistingNotification(LightlessNotification existing, LightlessNotification updated)
{
existing.Message = updated.Message;
existing.Progress = updated.Progress;
existing.ShowProgress = updated.ShowProgress;
existing.Title = updated.Title;
_logger.LogDebug("Updated existing notification: {Title}", updated.Title);
}
public void RemoveNotification(string id)
{
lock (_notificationLock)
{
var notification = _notifications.FirstOrDefault(n => n.Id == id);
if (notification != null)
{
StartOutAnimation(notification);
}
}
}
private void StartOutAnimation(LightlessNotification notification)
{
notification.IsAnimatingOut = true;
notification.IsAnimatingIn = false;
}
protected override void DrawInternal()
{
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero);
lock (_notificationLock)
{
UpdateNotifications();
if (_notifications.Count == 0)
{
ImGui.PopStyleVar();
IsOpen = false;
return;
}
var viewport = ImGui.GetMainViewport();
Position = CalculateWindowPosition(viewport);
DrawAllNotifications();
}
ImGui.PopStyleVar();
}
private Vector2 CalculateWindowPosition(ImGuiViewportPtr viewport)
{
var x = viewport.WorkPos.X + viewport.WorkSize.X -
_configService.Current.NotificationWidth -
_configService.Current.NotificationOffsetX -
WindowPaddingOffset;
var y = viewport.WorkPos.Y + _configService.Current.NotificationOffsetY;
return new Vector2(x, y);
}
private void DrawAllNotifications()
{
for (int i = 0; i < _notifications.Count; i++)
{
DrawNotification(_notifications[i], i);
if (i < _notifications.Count - 1)
{
ImGui.Dummy(new Vector2(0, _configService.Current.NotificationSpacing));
}
}
}
private void UpdateNotifications()
{
var deltaTime = ImGui.GetIO().DeltaTime;
EnforceMaxNotificationLimit();
UpdateAnimationsAndRemoveExpired(deltaTime);
}
private void EnforceMaxNotificationLimit()
{
var maxNotifications = _configService.Current.MaxSimultaneousNotifications;
while (_notifications.Count(n => !n.IsAnimatingOut) > maxNotifications)
{
var oldestNotification = _notifications
.Where(n => !n.IsAnimatingOut)
.OrderBy(n => n.CreatedAt)
.FirstOrDefault();
if (oldestNotification != null)
{
StartOutAnimation(oldestNotification);
}
}
}
private void UpdateAnimationsAndRemoveExpired(float deltaTime)
{
for (int i = _notifications.Count - 1; i >= 0; i--)
{
var notification = _notifications[i];
UpdateNotificationAnimation(notification, deltaTime);
if (ShouldRemoveNotification(notification))
{
_notifications.RemoveAt(i);
}
}
}
private void UpdateNotificationAnimation(LightlessNotification notification, float deltaTime)
{
if (notification.IsAnimatingIn && notification.AnimationProgress < 1f)
{
notification.AnimationProgress = Math.Min(1f,
notification.AnimationProgress + deltaTime * _configService.Current.NotificationAnimationSpeed);
}
else if (notification.IsAnimatingOut && notification.AnimationProgress > 0f)
{
notification.AnimationProgress = Math.Max(0f,
notification.AnimationProgress - deltaTime * _configService.Current.NotificationAnimationSpeed * OutAnimationSpeedMultiplier);
}
else if (!notification.IsAnimatingOut && !notification.IsDismissed)
{
notification.IsAnimatingIn = false;
if (notification.IsExpired)
{
StartOutAnimation(notification);
}
}
}
private bool ShouldRemoveNotification(LightlessNotification notification) =>
notification.IsAnimatingOut && notification.AnimationProgress <= 0.01f;
private void DrawNotification(LightlessNotification notification, int index)
{
var alpha = notification.AnimationProgress;
if (alpha <= 0f) return;
var slideOffset = (1f - alpha) * SlideAnimationDistance;
var originalCursorPos = ImGui.GetCursorPos();
ImGui.SetCursorPosX(originalCursorPos.X + slideOffset);
var notificationHeight = CalculateNotificationHeight(notification);
var notificationWidth = _configService.Current.NotificationWidth - slideOffset;
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero);
using var child = ImRaii.Child($"notification_{notification.Id}",
new Vector2(notificationWidth, notificationHeight),
false, ImGuiWindowFlags.NoScrollbar);
if (child.Success)
{
DrawNotificationContent(notification, alpha);
}
ImGui.PopStyleVar();
}
private void DrawNotificationContent(LightlessNotification notification, float alpha)
{
var drawList = ImGui.GetWindowDrawList();
var windowPos = ImGui.GetWindowPos();
var windowSize = ImGui.GetWindowSize();
var bgColor = CalculateBackgroundColor(alpha, ImGui.IsWindowHovered());
var accentColor = GetNotificationAccentColor(notification.Type);
accentColor.W *= alpha;
DrawShadow(drawList, windowPos, windowSize, alpha);
HandleClickToDismiss(notification);
DrawBackground(drawList, windowPos, windowSize, bgColor);
DrawAccentBar(drawList, windowPos, windowSize, accentColor);
DrawDurationProgressBar(notification, alpha, windowPos, windowSize, drawList);
DrawNotificationText(notification, alpha);
}
private Vector4 CalculateBackgroundColor(float alpha, bool isHovered)
{
var baseOpacity = _configService.Current.NotificationOpacity;
var finalOpacity = baseOpacity * alpha;
var bgColor = new Vector4(30f/255f, 30f/255f, 30f/255f, finalOpacity);
if (isHovered)
{
bgColor *= 1.1f;
bgColor.W = Math.Min(bgColor.W, 0.98f);
}
return bgColor;
}
private void DrawShadow(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float alpha)
{
var shadowOffset = new Vector2(1f, 1f);
var shadowColor = new Vector4(0f, 0f, 0f, 0.4f * alpha);
drawList.AddRectFilled(
windowPos + shadowOffset,
windowPos + windowSize + shadowOffset,
ImGui.ColorConvertFloat4ToU32(shadowColor),
3f
);
}
private void HandleClickToDismiss(LightlessNotification notification)
{
if (ImGui.IsWindowHovered() &&
_configService.Current.DismissNotificationOnClick &&
!notification.Actions.Any() &&
ImGui.IsMouseClicked(ImGuiMouseButton.Left))
{
notification.IsDismissed = true;
StartOutAnimation(notification);
}
}
private void DrawBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, Vector4 bgColor)
{
drawList.AddRectFilled(
windowPos,
windowPos + windowSize,
ImGui.ColorConvertFloat4ToU32(bgColor),
3f
);
}
private void DrawAccentBar(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, Vector4 accentColor)
{
var accentWidth = _configService.Current.NotificationAccentBarWidth;
if (accentWidth > 0f)
{
drawList.AddRectFilled(
windowPos,
windowPos + new Vector2(accentWidth, windowSize.Y),
ImGui.ColorConvertFloat4ToU32(accentColor),
3f
);
}
}
private void DrawDurationProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, ImDrawListPtr drawList)
{
var progress = CalculateProgress(notification);
var progressBarColor = UIColors.Get("LightlessBlue");
var progressHeight = 2f;
var progressY = windowPos.Y + windowSize.Y - progressHeight;
var progressWidth = windowSize.X * progress;
DrawProgressBackground(drawList, windowPos, windowSize, progressY, progressHeight, progressBarColor, alpha);
if (progress > 0)
{
DrawProgressForeground(drawList, windowPos, progressY, progressHeight, progressWidth, progressBarColor, alpha);
}
}
private float CalculateProgress(LightlessNotification notification)
{
if (notification.Type == NotificationType.Download && notification.ShowProgress)
{
return Math.Clamp(notification.Progress, 0f, 1f);
}
var elapsed = DateTime.UtcNow - notification.CreatedAt;
return Math.Min(1.0f, (float)(elapsed.TotalSeconds / notification.Duration.TotalSeconds));
}
private void DrawProgressBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float progressY, float progressHeight, Vector4 progressBarColor, float alpha)
{
var bgProgressColor = new Vector4(progressBarColor.X * 0.3f, progressBarColor.Y * 0.3f, progressBarColor.Z * 0.3f, 0.5f * alpha);
drawList.AddRectFilled(
new Vector2(windowPos.X, progressY),
new Vector2(windowPos.X + windowSize.X, progressY + progressHeight),
ImGui.ColorConvertFloat4ToU32(bgProgressColor),
0f
);
}
private void DrawProgressForeground(ImDrawListPtr drawList, Vector2 windowPos, float progressY, float progressHeight, float progressWidth, Vector4 progressBarColor, float alpha)
{
var progressColor = progressBarColor;
progressColor.W *= alpha;
drawList.AddRectFilled(
new Vector2(windowPos.X, progressY),
new Vector2(windowPos.X + progressWidth, progressY + progressHeight),
ImGui.ColorConvertFloat4ToU32(progressColor),
0f
);
}
private void DrawNotificationText(LightlessNotification notification, float alpha)
{
var padding = new Vector2(10f, 6f);
var contentPos = new Vector2(padding.X, padding.Y);
var windowSize = ImGui.GetWindowSize();
var contentSize = new Vector2(windowSize.X - padding.X, windowSize.Y - padding.Y * 2);
ImGui.SetCursorPos(contentPos);
var titleHeight = DrawTitle(notification, contentSize.X, alpha);
DrawMessage(notification, contentPos, contentSize.X, titleHeight, alpha);
if (notification.Actions.Count > 0)
{
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + ImGui.GetStyle().ItemSpacing.Y);
ImGui.SetCursorPosX(contentPos.X);
DrawNotificationActions(notification, contentSize.X, alpha);
}
}
private float DrawTitle(LightlessNotification notification, float contentWidth, float alpha)
{
using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(1f, 1f, 1f, alpha)))
{
ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + contentWidth);
var titleStartY = ImGui.GetCursorPosY();
var titleText = _configService.Current.ShowNotificationTimestamp
? $"[{notification.CreatedAt.ToLocalTime():HH:mm:ss}] {notification.Title}"
: notification.Title;
ImGui.TextWrapped(titleText);
var titleHeight = ImGui.GetCursorPosY() - titleStartY;
ImGui.PopTextWrapPos();
return titleHeight;
}
}
private void DrawMessage(LightlessNotification notification, Vector2 contentPos, float contentWidth, float titleHeight, float alpha)
{
if (string.IsNullOrEmpty(notification.Message)) return;
ImGui.SetCursorPos(contentPos + new Vector2(0f, titleHeight + 4f));
ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + contentWidth);
using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(0.9f, 0.9f, 0.9f, alpha)))
{
ImGui.TextWrapped(notification.Message);
}
ImGui.PopTextWrapPos();
}
private void DrawNotificationActions(LightlessNotification notification, float availableWidth, float alpha)
{
var buttonSpacing = 8f;
var rightPadding = 10f;
var usableWidth = availableWidth - rightPadding;
var totalSpacing = (notification.Actions.Count - 1) * buttonSpacing;
var buttonWidth = (usableWidth - totalSpacing) / notification.Actions.Count;
_logger.LogDebug("Drawing {ActionCount} notification actions, buttonWidth: {ButtonWidth}, availableWidth: {AvailableWidth}",
notification.Actions.Count, buttonWidth, availableWidth);
var startCursorPos = ImGui.GetCursorPos();
for (int i = 0; i < notification.Actions.Count; i++)
{
var action = notification.Actions[i];
if (i > 0)
{
ImGui.SameLine();
var currentX = startCursorPos.X + i * (buttonWidth + buttonSpacing);
ImGui.SetCursorPosX(currentX);
}
DrawActionButton(action, notification, alpha, buttonWidth);
}
}
private void DrawActionButton(LightlessNotificationAction action, LightlessNotification notification, float alpha, float buttonWidth)
{
_logger.LogDebug("Drawing action button: {ActionId} - {ActionLabel}, width: {ButtonWidth}", action.Id, action.Label, buttonWidth);
var buttonColor = action.Color;
buttonColor.W *= alpha;
var hoveredColor = buttonColor * 1.1f;
hoveredColor.W = buttonColor.W;
var activeColor = buttonColor * 0.9f;
activeColor.W = buttonColor.W;
using (ImRaii.PushColor(ImGuiCol.Button, buttonColor))
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, hoveredColor))
using (ImRaii.PushColor(ImGuiCol.ButtonActive, activeColor))
using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(1f, 1f, 1f, alpha)))
{
var buttonPressed = false;
if (action.Icon != FontAwesomeIcon.None)
{
buttonPressed = DrawIconTextButton(action.Icon, action.Label, buttonWidth, alpha);
}
else
{
buttonPressed = ImGui.Button(action.Label, new Vector2(buttonWidth, 0));
}
_logger.LogDebug("Button {ActionId} pressed: {ButtonPressed}", action.Id, buttonPressed);
if (buttonPressed)
{
try
{
_logger.LogDebug("Executing action: {ActionId}", action.Id);
action.OnClick(notification);
_logger.LogDebug("Action executed successfully: {ActionId}", action.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error executing notification action: {ActionId}", action.Id);
}
}
}
}
private bool DrawIconTextButton(FontAwesomeIcon icon, string text, float width, float alpha)
{
var drawList = ImGui.GetWindowDrawList();
var cursorPos = ImGui.GetCursorScreenPos();
var frameHeight = ImGui.GetFrameHeight();
Vector2 iconSize;
using (ImRaii.PushFont(UiBuilder.IconFont))
{
iconSize = ImGui.CalcTextSize(icon.ToIconString());
}
var textSize = ImGui.CalcTextSize(text);
var spacing = 3f * ImGuiHelpers.GlobalScale;
var totalTextWidth = iconSize.X + spacing + textSize.X;
var buttonPressed = ImGui.InvisibleButton($"btn_{icon}_{text}", new Vector2(width, frameHeight));
var buttonMin = ImGui.GetItemRectMin();
var buttonMax = ImGui.GetItemRectMax();
var buttonSize = buttonMax - buttonMin;
var buttonColor = ImGui.GetColorU32(ImGuiCol.Button);
if (ImGui.IsItemHovered())
buttonColor = ImGui.GetColorU32(ImGuiCol.ButtonHovered);
if (ImGui.IsItemActive())
buttonColor = ImGui.GetColorU32(ImGuiCol.ButtonActive);
drawList.AddRectFilled(buttonMin, buttonMax, buttonColor, 3f);
var iconPos = buttonMin + new Vector2((buttonSize.X - totalTextWidth) / 2f, (buttonSize.Y - iconSize.Y) / 2f);
var textPos = iconPos + new Vector2(iconSize.X + spacing, (iconSize.Y - textSize.Y) / 2f);
var textColor = ImGui.GetColorU32(ImGuiCol.Text);
// Draw icon
using (ImRaii.PushFont(UiBuilder.IconFont))
{
drawList.AddText(iconPos, textColor, icon.ToIconString());
}
// Draw text
drawList.AddText(textPos, textColor, text);
return buttonPressed;
}
private float CalculateNotificationHeight(LightlessNotification notification)
{
var contentWidth = _configService.Current.NotificationWidth - 35f;
var height = 12f;
height += CalculateTitleHeight(notification, contentWidth);
height += CalculateMessageHeight(notification, contentWidth);
if (notification.ShowProgress)
{
height += 12f;
}
if (notification.Actions.Count > 0)
{
height += ImGui.GetStyle().ItemSpacing.Y;
height += ImGui.GetFrameHeight();
height += 12f;
}
return Math.Clamp(height, NotificationMinHeight, NotificationMaxHeight);
}
private float CalculateTitleHeight(LightlessNotification notification, float contentWidth)
{
var titleText = _configService.Current.ShowNotificationTimestamp
? $"[{notification.CreatedAt.ToLocalTime():HH:mm:ss}] {notification.Title}"
: notification.Title;
return ImGui.CalcTextSize(titleText, true, contentWidth).Y;
}
private float CalculateMessageHeight(LightlessNotification notification, float contentWidth)
{
if (string.IsNullOrEmpty(notification.Message)) return 0f;
var messageHeight = ImGui.CalcTextSize(notification.Message, true, contentWidth).Y;
return 4f + messageHeight;
}
private Vector4 GetNotificationAccentColor(NotificationType type)
{
return type switch
{
NotificationType.Info => UIColors.Get("LightlessPurple"),
NotificationType.Warning => UIColors.Get("LightlessYellow"),
NotificationType.Error => UIColors.Get("DimRed"),
NotificationType.PairRequest => UIColors.Get("LightlessBlue"),
NotificationType.Download => UIColors.Get("LightlessGreen"),
_ => UIColors.Get("LightlessPurple")
};
}
}

View File

@@ -1,32 +0,0 @@
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

@@ -1,72 +0,0 @@
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,231 +1,169 @@
// 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
{ {
public readonly record struct StyleColorOption(string Key, string Label, Func<Vector4> DefaultValue, ImGuiCol Target, string? Description = null, string? UiColorKey = null); internal static class MainStyle
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)
{ {
_config = config; private static LightlessConfigService? _config;
_themeConfig = themeConfig; public static void Init(LightlessConfigService config) => _config = config;
} public static bool ShouldUseTheme => _config?.Current.UseLightlessRedesign ?? false;
public static bool ShouldUseTheme => _config?.Current.UseLightlessRedesign ?? false;
private static bool _hasPushed; private static bool _hasPushed;
private static int _pushedColorCount; private static int _pushedColorCount;
private static int _pushedStyleVarCount; private static int _pushedStyleVarCount;
private static readonly StyleColorOption[] _colorOptions = public static void PushStyle()
[
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)
{ {
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; _hasPushed = false;
return; _pushedColorCount = 0;
_pushedStyleVarCount = 0;
} }
_hasPushed = true; private static void Push(ImGuiCol col, Vector4 rgba)
_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 (rgba.X > 1f || rgba.Y > 1f || rgba.Z > 1f || rgba.W > 1f)
rgba /= 255f;
ImGui.PushStyleColor(col, rgba);
_pushedColorCount++;
} }
if (option.Min is { } min) private static void Push(ImGuiCol col, uint packedRgba)
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; ImGui.PushStyleColor(col, packedRgba);
_pushedColorCount++;
} }
if (option.Min.HasValue) private static void PushStyleVar(ImGuiStyleVar var, float value)
value = MathF.Max(option.Min.Value, value); {
if (option.Max.HasValue) ImGui.PushStyleVar(var, value);
value = MathF.Min(option.Max.Value, value); _pushedStyleVarCount++;
return value; }
private static void PushStyleVar(ImGuiStyleVar var, Vector2 value)
{
ImGui.PushStyleVar(var, value);
_pushedStyleVarCount++;
}
} }
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

@@ -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,8 +11,12 @@ 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;
@@ -29,14 +33,13 @@ 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, NotificationService lightlessNotificationService) public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService)
{ {
_lightlessMediator = lightlessMediator; _lightlessMediator = lightlessMediator;
_apiController = apiController; _apiController = apiController;
@@ -44,7 +47,6 @@ public class TopTabMenu
_pairRequestService = pairRequestService; _pairRequestService = pairRequestService;
_dalamudUtilService = dalamudUtilService; _dalamudUtilService = dalamudUtilService;
_uiSharedService = uiSharedService; _uiSharedService = uiSharedService;
_lightlessNotificationService = lightlessNotificationService;
} }
private enum SelectedTab private enum SelectedTab
@@ -197,79 +199,16 @@ public class TopTabMenu
if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f); if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f);
#if DEBUG #if DEBUG
if (ImGui.Button("Test Pair Request")) if (ImGui.Button("Add Test Pair Request"))
{
_lightlessNotificationService.ShowPairRequestNotification(
"Debug User",
"debug-user-id",
onAccept: () =>
{
_lightlessMediator.Publish(new NotificationMessage(
"Pair Accepted",
"Debug pair request was accepted!",
NotificationType.Info,
TimeSpan.FromSeconds(3)));
},
onDecline: () =>
{
_lightlessMediator.Publish(new NotificationMessage(
"Pair Declined",
"Debug pair request was declined.",
NotificationType.Warning,
TimeSpan.FromSeconds(3)));
}
);
}
ImGui.SameLine();
if (ImGui.Button("Test Info"))
{ {
var fakeCid = Guid.NewGuid().ToString("N");
var display = _pairRequestService.RegisterIncomingRequest(fakeCid, "Debug pair request");
_lightlessMediator.Publish(new NotificationMessage( _lightlessMediator.Publish(new NotificationMessage(
"Information", "Pair request received (debug)",
"This is a test ifno notification with some longer text to see how it wraps. This is a test ifno notification with some longer text to see how it wraps. This is a test ifno notification with some longer text to see how it wraps. This is a test ifno notification with some longer text to see how it wraps.", display.Message,
NotificationType.Info, NotificationType.Info,
TimeSpan.FromSeconds(5))); TimeSpan.FromSeconds(5)));
} }
ImGui.SameLine();
if (ImGui.Button("Test Warning"))
{
_lightlessMediator.Publish(new NotificationMessage(
"Warning",
"This is a test warning notification.",
NotificationType.Warning,
TimeSpan.FromSeconds(7)));
}
ImGui.SameLine();
if (ImGui.Button("Test Error"))
{
_lightlessMediator.Publish(new NotificationMessage(
"Error",
"This is a test error notification erp police",
NotificationType.Error,
TimeSpan.FromSeconds(10)));
}
if (ImGui.Button("Test Download Progress"))
{
var downloadStatus = new List<(string playerName, float progress, string status)>
{
("Mauwmauw Nekochan", 0.85f, "downloading"),
("Raelynn Kitsune", 0.34f, "downloading"),
("Jaina Elraeth", 0.67f, "downloading"),
("Vaelstra Bloodthorn", 0.19f, "downloading"),
("Lydia Hera Moondrop", 0.86f, "downloading"),
("C'liina Star", 1.0f, "completed")
};
_lightlessNotificationService.ShowPairDownloadNotification(downloadStatus);
}
ImGui.SameLine();
if (ImGui.Button("Dismiss Download"))
{
_lightlessNotificationService.DismissPairDownloadNotification();
}
#endif #endif
DrawIncomingPairRequests(availableWidth); DrawIncomingPairRequests(availableWidth);
@@ -462,7 +401,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).ConfigureAwait(false); await _apiController.TryPairWithContentId(request.HashedCid, myCidHash).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

@@ -26,9 +26,6 @@ namespace LightlessSync.UI
{ "LightlessAdminGlow", "#b09343" }, { "LightlessAdminGlow", "#b09343" },
{ "LightlessModeratorText", "#94ffda" }, { "LightlessModeratorText", "#94ffda" },
{ "LightlessModeratorGlow", "#599c84" }, { "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) public async Task TryPairWithContentId(string otherCid, string myCid)
{ {
if (!IsConnected) return; if (!IsConnected) return;
await _lightlessHub!.SendAsync(nameof(TryPairWithContentId), otherCid).ConfigureAwait(false); await _lightlessHub!.SendAsync(nameof(TryPairWithContentId), otherCid, myCid).ConfigureAwait(false);
} }
public async Task SetBroadcastStatus(bool enabled, GroupBroadcastRequestDto? groupDto = null) public async Task SetBroadcastStatus(string hashedCid, bool enabled, GroupBroadcastRequestDto? groupDto = null)
{ {
CheckConnection(); CheckConnection();
await _lightlessHub!.InvokeAsync(nameof(SetBroadcastStatus), enabled, groupDto).ConfigureAwait(false); await _lightlessHub!.InvokeAsync(nameof(SetBroadcastStatus), hashedCid, 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() public async Task<TimeSpan?> GetBroadcastTtl(string hashedCid)
{ {
CheckConnection(); CheckConnection();
return await _lightlessHub!.InvokeAsync<TimeSpan?>(nameof(GetBroadcastTtl)).ConfigureAwait(false); return await _lightlessHub!.InvokeAsync<TimeSpan?>(nameof(GetBroadcastTtl), hashedCid).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,7 +6,6 @@ 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;
@@ -105,27 +104,25 @@ 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); var request = _pairRequestService.RegisterIncomingRequest(dto.myHashedCid, dto.message ?? string.Empty);
var senderName = string.IsNullOrEmpty(request.DisplayName) ? "Unknown User" : request.DisplayName;
_lightlessNotificationService.ShowPairRequestNotification( Mediator.Publish(new NotificationMessage(
senderName, "Pair request received",
request.HashedCid, request.Message,
onAccept: () => _pairRequestService.AcceptPairRequest(request.HashedCid, senderName), NotificationType.Info,
onDecline: () => _pairRequestService.DeclinePairRequest(request.HashedCid)); 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,7 +32,6 @@ 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;
@@ -45,7 +44,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, NotificationService lightlessNotificationService) : base(logger, mediator) TokenProvider tokenProvider, LightlessConfigService lightlessConfigService) : base(logger, mediator)
{ {
_hubFactory = hubFactory; _hubFactory = hubFactory;
_dalamudUtil = dalamudUtil; _dalamudUtil = dalamudUtil;
@@ -54,7 +53,6 @@ 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());