Compare commits
135 Commits
1.12.1-bet
...
1.12.2.7-D
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e129f6a1d | ||
|
|
1e97f27cb8 | ||
|
|
7aadbcec10 | ||
|
|
a32ac02c6d | ||
| f48698373b | |||
| ee20b6fa5f | |||
| f11741225b | |||
|
|
147baa4c1b | ||
|
|
4f5ef8ff4b | ||
|
|
fae6d31792 | ||
|
|
b4dd0ee0e1 | ||
| f5458c7f97 | |||
| 923f118a47 | |||
|
|
0cb71e5444 | ||
|
|
268fd471fe | ||
|
|
d517a21f5d | ||
| cac94374d9 | |||
|
|
b513e0555b | ||
|
|
de8c9cf035 | ||
|
|
7f8872cbe0 | ||
|
|
5626a34755 | ||
|
|
a98afdda01 | ||
| 4373092d44 | |||
| 7b5c61371e | |||
| 9bd997f699 | |||
| e2511a5c1f | |||
|
|
e8760a8937 | ||
|
|
7d4e097be8 | ||
|
|
2cba1ccfe0 | ||
| 06921e1dd1 | |||
|
|
68ba5f4b06 | ||
|
|
217c160ec7 | ||
|
|
ff010fa1f8 | ||
| f7145339b3 | |||
|
|
aa2b828386 | ||
|
|
547db3a76b | ||
|
|
4ac8b24524 | ||
|
|
d72cc207e1 | ||
|
|
477f5aa6e7 | ||
|
|
edb7232b17 | ||
|
|
44177ab7bd | ||
|
|
47b7ecd521 | ||
|
|
8a3902ec2b | ||
|
|
ea8f8e3895 | ||
|
|
f1af6601cc | ||
|
|
2d094404df | ||
|
|
8fdff1eb18 | ||
|
|
9170b5205c | ||
|
|
dccd2cdc36 | ||
|
|
6d01d47c2f | ||
|
|
280c80d89f | ||
|
|
92b8d4a1cd | ||
|
|
f77d109d00 | ||
|
|
823dd39a9b | ||
|
|
d5c12e81c3 | ||
|
|
04c00af92e | ||
|
|
a66a43dda8 | ||
| 77ff8ae372 | |||
|
|
011cf7951b | ||
|
|
7d480b9e2c | ||
|
|
cf27a67296 | ||
|
|
d6a4595bb8 | ||
|
|
f202818b55 | ||
|
|
3f2e4d6640 | ||
|
|
90b483e4ea | ||
| 434c7d5f4a | |||
|
|
a4eb840589 | ||
|
|
e80806ef9d | ||
|
|
c447c33b7a | ||
|
|
bcb524df52 | ||
| b64bb66119 | |||
|
|
118edb9dea | ||
|
|
b43ceb9f7e | ||
|
|
6c0d00dc39 | ||
|
|
be847c16b8 | ||
| 02a680f8cc | |||
|
|
dae8127ac8 | ||
|
|
0635caab65 | ||
| 1530ac3911 | |||
| 3f80467180 | |||
|
|
4b4e587a89 | ||
|
|
02c3846031 | ||
|
|
a8a01b3034 | ||
| 50a5046c96 | |||
|
|
c0b8e15380 | ||
| e6735be594 | |||
|
|
fe419336d7 | ||
|
|
ffbeeba929 | ||
|
|
a7475a7007 | ||
|
|
3936cbd439 | ||
|
|
ba16963b66 | ||
| 6467a3e73b | |||
|
|
bb779904f7 | ||
|
|
59d0e8ee37 | ||
|
|
a441bbfcc8 | ||
|
|
c545ccea52 | ||
|
|
c32d9cadff | ||
|
|
d7c9df54cb | ||
| 37ec0961d9 | |||
| 9736c5090d | |||
|
|
4f3ab604db | ||
|
|
6a0f8c507c | ||
|
|
e13fde3d43 | ||
| 7b806ab660 | |||
|
|
387e5ad515 | ||
|
|
70c296a16b | ||
|
|
2a9b5812ed | ||
|
|
9b04976aa6 | ||
|
|
144ac166fb | ||
|
|
98c3a2c7f8 | ||
|
|
b06ffb3341 | ||
| e9461efe11 | |||
| 1f1afdec24 | |||
| d428a436e7 | |||
|
|
ad29fa7b69 | ||
|
|
23c56505ac | ||
|
|
58850f4530 | ||
|
|
f5339dc1d2 | ||
|
|
85ecea6391 | ||
|
|
cd817487e4 | ||
|
|
f50b622f0a | ||
|
|
d295f3e22d | ||
|
|
0dfa667ed3 | ||
|
|
2b118df892 | ||
|
|
f01229a97f | ||
|
|
3fdc9dd958 | ||
|
|
86107acf12 | ||
|
|
27e7fb7ed9 | ||
|
|
17f4ddad89 | ||
| a29e155cec | |||
|
|
1488704db4 | ||
|
|
46db5c87e0 | ||
|
|
9b6d00570e | ||
|
|
83e4555e4b | ||
|
|
090b81c989 |
@@ -41,9 +41,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Get version
|
- name: Get version
|
||||||
id: package_version
|
id: package_version
|
||||||
uses: KageKirin/get-csproj-version@v0
|
run: |
|
||||||
with:
|
version=$(grep -oPm1 "(?<=<Version>)[^<]+" LightlessSync/LightlessSync.csproj)
|
||||||
file: LightlessSync/LightlessSync.csproj
|
echo "version=$version" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Display version
|
- name: Display version
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
Submodule LightlessAPI updated: 6c542c0ccc...0bc7abb274
179
LightlessSync/Changelog/changelog.yaml
Normal file
179
LightlessSync/Changelog/changelog.yaml
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
tagline: "Lightless Sync v1.12.3"
|
||||||
|
subline: "LightSpeed, Welcome Screen, and More!"
|
||||||
|
changelog:
|
||||||
|
- name: "v1.12.3"
|
||||||
|
tagline: "LightSpeed, Welcome Screen, and More!"
|
||||||
|
date: "October 15th 2025"
|
||||||
|
# be sure to set this every new version
|
||||||
|
isCurrent: true
|
||||||
|
versions:
|
||||||
|
- number: "LightSpeed"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "New way to download that will download mods directly from the file server"
|
||||||
|
- "LightSpeed is in BETA and should be faster than the batch downloading"
|
||||||
|
- number: "Welcome Screen + Additional Features"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "New in-game Patch Notes window."
|
||||||
|
- "Credits section to thank contributors and supporters."
|
||||||
|
- "Patch notes only show after updates, not during first-time setup."
|
||||||
|
- "Syncshell Rework stared: Profiles have been added (more features using this will come later)."
|
||||||
|
- number: "Notifications"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "More customizable notification options."
|
||||||
|
- "Perfomance limiter shows as notifications."
|
||||||
|
- "All notifications can be configured or disabled in Settings → Notifications."
|
||||||
|
- "Cleaning up notifications implementation"
|
||||||
|
- number: "Bugfixes"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "Added more safety checks to nameplates"
|
||||||
|
- "Removed a line in SyncshellUI potentially causing NullPointers"
|
||||||
|
- "Additional safety checks in PlayerData.Factory"
|
||||||
|
- name: "v1.12.2"
|
||||||
|
tagline: "LightFinder fixes, Notifications overhaul"
|
||||||
|
date: "October 12th 2025"
|
||||||
|
versions:
|
||||||
|
- number: "LightFinder"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "Server-side improvements for LightFinder functionality."
|
||||||
|
- "Command changed from '/light lightfinder' to '/light finder'."
|
||||||
|
- "Option to enable LightFinder on connection (opt-in, refreshes every 3 hours)."
|
||||||
|
- "LightFinder indicator can now be shown on the server info bar."
|
||||||
|
- number: "Notifications"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "Completely reworked notification system with new UI."
|
||||||
|
- "Pair requests now show as notifications."
|
||||||
|
- "Download progress shows as notifications."
|
||||||
|
- "Customizable notification sounds, size, position, and duration."
|
||||||
|
- "All notifications can be configured or disabled in Settings → Notifications."
|
||||||
|
- number: "Bug Fixes"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "Fixed nameplate alignment issues with LightFinder and icons."
|
||||||
|
- "Icons now properly apply instead of swapping on choice."
|
||||||
|
- "Updated Discord URL."
|
||||||
|
- "File cache logic improvements."
|
||||||
|
|
||||||
|
- name: "v1.12.1"
|
||||||
|
tagline: "LightFinder customization and download limiter"
|
||||||
|
date: "October 8th 2025"
|
||||||
|
versions:
|
||||||
|
- number: "New Features"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "LightFinder text can be modified to an icon with customizable positioning."
|
||||||
|
- "Option to hide your own indicator or paired player indicators."
|
||||||
|
- "Pair Download Limiter: Limit simultaneous downloads to 1-6 users to reduce network strain."
|
||||||
|
- "Added '/light lightfinder' command to open LightFinder UI."
|
||||||
|
- number: "Improvements"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "Right-click menu option for Send Pair Request can be disabled."
|
||||||
|
- "Syncshell finder improvements."
|
||||||
|
- "Download limiter settings available in Settings → Transfers."
|
||||||
|
|
||||||
|
- name: "v1.12.0"
|
||||||
|
tagline: "LightFinder - Major feature release"
|
||||||
|
date: "October 5th 2025"
|
||||||
|
versions:
|
||||||
|
- number: "Major Features"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "Introduced LightFinder: Optional feature inspired by FFXIV's Party Finder."
|
||||||
|
- "Find fellow Lightless users and advertise your Syncshell to others."
|
||||||
|
- "When enabled, you're visible to other LightFinder users for 3 hours."
|
||||||
|
- "LightFinder tag displays above your nameplate when active."
|
||||||
|
- "Receive pair requests directly in UI without exchanging UIDs."
|
||||||
|
- "Syncshell Finder allows joining indexed Syncshells."
|
||||||
|
- "[L] Send Pair Request added to player context menus."
|
||||||
|
- number: "Vanity Features"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "Supporters can now customize their name color in the Lightless UI."
|
||||||
|
- "Color changes visible to all users."
|
||||||
|
- number: "General Improvements"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "Pairing nameplate color override can now override FC tags."
|
||||||
|
- "Added .kdb as whitelisted filetype for uploads."
|
||||||
|
- "Various UI fixes, updates, and improvements."
|
||||||
|
|
||||||
|
- name: "v1.11.12"
|
||||||
|
tagline: "Syncshell grouping and performance options"
|
||||||
|
date: "September 16th 2025"
|
||||||
|
versions:
|
||||||
|
- number: "New Features"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "Ability to show grouped syncshells in main UI/all syncshells (default ON)."
|
||||||
|
- "Transfer ownership button available in Admin Panel user list."
|
||||||
|
- "Self-threshold warning now opens character analysis screen when clicked."
|
||||||
|
- number: "Performance"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "Auto-pause combat and auto-pause performance are now optional settings."
|
||||||
|
- "Both options are auto-enabled by default - disable at your own risk."
|
||||||
|
- number: "Bug Fixes"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "Reworked file caching to reduce errors for some users."
|
||||||
|
- "Fixed bug where exiting PvP could desync some users."
|
||||||
|
|
||||||
|
- name: "v1.11.9"
|
||||||
|
tagline: "File cache improvements"
|
||||||
|
date: "September 13th 2025"
|
||||||
|
versions:
|
||||||
|
- number: "Bug Fixes"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "Identified and fixed potential file cache problems."
|
||||||
|
- "Improved cache error handling and stability."
|
||||||
|
|
||||||
|
- name: "v1.11.8"
|
||||||
|
tagline: "Hotfix - UI and exception handling"
|
||||||
|
date: "September 12th 2025"
|
||||||
|
versions:
|
||||||
|
- number: "Bug Fixes"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "Attempted fix for NullReferenceException spam."
|
||||||
|
- "Fixed additional UI edge cases preventing loading for some users."
|
||||||
|
- "Fixed color bar UI issues."
|
||||||
|
|
||||||
|
- name: "v1.11.7"
|
||||||
|
tagline: "Hotfix - UI loading and warnings"
|
||||||
|
date: "September 12th 2025"
|
||||||
|
versions:
|
||||||
|
- number: "Bug Fixes"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "Fixed UI not loading for some users."
|
||||||
|
- "Self warnings now behind 'Warn on loading in players exceeding performance thresholds' setting."
|
||||||
|
|
||||||
|
- name: "v1.11.6"
|
||||||
|
tagline: "Admin panel rework and new features"
|
||||||
|
date: "September 11th 2025"
|
||||||
|
versions:
|
||||||
|
- number: "New Features"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "Reworked Syncshell Admin Page with improved styling."
|
||||||
|
- "Right-click on Server Top Bar button to disconnect from Lightless."
|
||||||
|
- "Shift+Left click on Server Top Bar button to open settings."
|
||||||
|
- "Added colors section in settings to change accent colors."
|
||||||
|
- "Ability to pause syncing while in Instance/Duty."
|
||||||
|
- "Functionality to create syncshell folders."
|
||||||
|
- "Added self-threshold warning."
|
||||||
|
- number: "Bug Fixes"
|
||||||
|
icon: ""
|
||||||
|
items:
|
||||||
|
- "Fixed owners being visible in moderator list view."
|
||||||
|
- "Removed Pin/Remove/Ban buttons on Owners when viewing as moderator."
|
||||||
|
- "Fixed nameplate bug in PvP."
|
||||||
|
- "Added 1 or 3 day options for inactive check."
|
||||||
|
- "Fixed bug where some users could not see their own syncshell folders."
|
||||||
68
LightlessSync/Changelog/credits.yaml
Normal file
68
LightlessSync/Changelog/credits.yaml
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
credits:
|
||||||
|
- category: "Development Team"
|
||||||
|
items:
|
||||||
|
- name: "Abel"
|
||||||
|
role: "Developer"
|
||||||
|
- name: "Cake"
|
||||||
|
role: "Developer"
|
||||||
|
- name: "Celine"
|
||||||
|
role: "Developer"
|
||||||
|
- name: "Choco"
|
||||||
|
role: "Developer"
|
||||||
|
- name: "Kenny"
|
||||||
|
role: "Developer"
|
||||||
|
- name: "Zura"
|
||||||
|
role: "Developer"
|
||||||
|
- name: "Additional Contributors"
|
||||||
|
role: "Community Contributors & Bug Reporters"
|
||||||
|
|
||||||
|
- category: "Moderation Team"
|
||||||
|
items:
|
||||||
|
- name: "Crow"
|
||||||
|
role: "Moderator"
|
||||||
|
- name: "Faith"
|
||||||
|
role: "Moderator"
|
||||||
|
- name: "Kiwiwiwi"
|
||||||
|
role: "Moderator"
|
||||||
|
- name: "Kruwu"
|
||||||
|
role: "Moderator"
|
||||||
|
- name: "Lexi"
|
||||||
|
role: "Moderator"
|
||||||
|
- name: "Maya"
|
||||||
|
role: "Moderator"
|
||||||
|
- name: "Metaknight"
|
||||||
|
role: "Moderator"
|
||||||
|
- name: "Minmoose"
|
||||||
|
role: "Moderator"
|
||||||
|
- name: "Nihal"
|
||||||
|
role: "Moderator"
|
||||||
|
- name: "Tani"
|
||||||
|
role: "Moderator"
|
||||||
|
|
||||||
|
- category: "Plugin Integration & IPC Support"
|
||||||
|
items:
|
||||||
|
- name: "Penumbra Team"
|
||||||
|
role: "Mod framework integration"
|
||||||
|
- name: "Glamourer Team"
|
||||||
|
role: "Customization system integration"
|
||||||
|
- name: "Customize+ Team"
|
||||||
|
role: "Body scaling integration"
|
||||||
|
- name: "Simple Heels Team"
|
||||||
|
role: "Height offset integration"
|
||||||
|
- name: "Honorific Team"
|
||||||
|
role: "Title system integration"
|
||||||
|
- name: "Glyceri"
|
||||||
|
role: "Moodles - Status effect integration"
|
||||||
|
- name: "Glyceri"
|
||||||
|
role: "PetNicknames - Pet naming integration"
|
||||||
|
- name: "Minmoose"
|
||||||
|
role: "Brio - GPose enhancement integration"
|
||||||
|
|
||||||
|
- category: "Special Thanks"
|
||||||
|
items:
|
||||||
|
- name: "Dalamud & XIVLauncher Teams"
|
||||||
|
role: "Plugin framework and infrastructure"
|
||||||
|
- name: "Community Supporters"
|
||||||
|
role: "Testing, feedback, and financial support"
|
||||||
|
- name: "Beta Testers"
|
||||||
|
role: "Early testing and bug reporting"
|
||||||
@@ -16,6 +16,8 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
public const string CachePrefix = "{cache}";
|
public const string CachePrefix = "{cache}";
|
||||||
public const string CsvSplit = "|";
|
public const string CsvSplit = "|";
|
||||||
public const string PenumbraPrefix = "{penumbra}";
|
public const string PenumbraPrefix = "{penumbra}";
|
||||||
|
private const int FileCacheVersion = 1;
|
||||||
|
private const string FileCacheVersionHeaderPrefix = "#lightless-file-cache-version:";
|
||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly LightlessMediator _lightlessMediator;
|
private readonly LightlessMediator _lightlessMediator;
|
||||||
private readonly string _csvPath;
|
private readonly string _csvPath;
|
||||||
@@ -25,6 +27,7 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
private readonly Lock _fileWriteLock = new();
|
private readonly Lock _fileWriteLock = new();
|
||||||
private readonly IpcManager _ipcManager;
|
private readonly IpcManager _ipcManager;
|
||||||
private readonly ILogger<FileCacheManager> _logger;
|
private readonly ILogger<FileCacheManager> _logger;
|
||||||
|
private bool _csvHeaderEnsured;
|
||||||
public string CacheFolder => _configService.Current.CacheFolder;
|
public string CacheFolder => _configService.Current.CacheFolder;
|
||||||
|
|
||||||
public FileCacheManager(ILogger<FileCacheManager> logger, IpcManager ipcManager, LightlessConfigService configService, LightlessMediator lightlessMediator)
|
public FileCacheManager(ILogger<FileCacheManager> logger, IpcManager ipcManager, LightlessConfigService configService, LightlessMediator lightlessMediator)
|
||||||
@@ -54,6 +57,62 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
return NormalizeSeparators(prefixedPath).ToLowerInvariant();
|
return NormalizeSeparators(prefixedPath).ToLowerInvariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryBuildPrefixedPath(string path, string? baseDirectory, string prefix, out string prefixedPath, out int matchedLength)
|
||||||
|
{
|
||||||
|
prefixedPath = string.Empty;
|
||||||
|
matchedLength = 0;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(baseDirectory))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedPath = NormalizeSeparators(path).ToLowerInvariant();
|
||||||
|
var normalizedBase = NormalizeSeparators(baseDirectory).TrimEnd('\\').ToLowerInvariant();
|
||||||
|
|
||||||
|
if (!normalizedPath.StartsWith(normalizedBase, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedPath.Length > normalizedBase.Length)
|
||||||
|
{
|
||||||
|
if (normalizedPath[normalizedBase.Length] != '\\')
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
prefixedPath = prefix + normalizedPath.Substring(normalizedBase.Length);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
prefixedPath = prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
prefixedPath = prefixedPath.Replace("\\\\", "\\", StringComparison.Ordinal);
|
||||||
|
matchedLength = normalizedBase.Length;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildVersionHeader() => $"{FileCacheVersionHeaderPrefix}{FileCacheVersion}";
|
||||||
|
|
||||||
|
private static bool TryParseVersionHeader(string? line, out int version)
|
||||||
|
{
|
||||||
|
version = 0;
|
||||||
|
if (string.IsNullOrWhiteSpace(line))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!line.StartsWith(FileCacheVersionHeaderPrefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var versionSpan = line.AsSpan(FileCacheVersionHeaderPrefix.Length);
|
||||||
|
return int.TryParse(versionSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out version);
|
||||||
|
}
|
||||||
|
|
||||||
private string NormalizeToPrefixedPath(string path)
|
private string NormalizeToPrefixedPath(string path)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(path)) return string.Empty;
|
if (string.IsNullOrEmpty(path)) return string.Empty;
|
||||||
@@ -66,27 +125,25 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
return NormalizePrefixedPathKey(normalized);
|
return NormalizePrefixedPathKey(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
var penumbraDir = _ipcManager.Penumbra.ModDirectory;
|
string? chosenPrefixed = null;
|
||||||
if (!string.IsNullOrEmpty(penumbraDir))
|
var chosenLength = -1;
|
||||||
|
|
||||||
|
if (TryBuildPrefixedPath(normalized, _ipcManager.Penumbra.ModDirectory, PenumbraPrefix, out var penumbraPrefixed, out var penumbraMatch))
|
||||||
{
|
{
|
||||||
var normalizedPenumbra = NormalizeSeparators(penumbraDir);
|
chosenPrefixed = penumbraPrefixed;
|
||||||
var replacement = normalizedPenumbra.EndsWith("\\", StringComparison.Ordinal)
|
chosenLength = penumbraMatch;
|
||||||
? PenumbraPrefix + "\\"
|
|
||||||
: PenumbraPrefix;
|
|
||||||
normalized = normalized.Replace(normalizedPenumbra, replacement, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var cacheFolder = _configService.Current.CacheFolder;
|
if (TryBuildPrefixedPath(normalized, _configService.Current.CacheFolder, CachePrefix, out var cachePrefixed, out var cacheMatch))
|
||||||
if (!string.IsNullOrEmpty(cacheFolder))
|
|
||||||
{
|
{
|
||||||
var normalizedCache = NormalizeSeparators(cacheFolder);
|
if (cacheMatch > chosenLength)
|
||||||
var replacement = normalizedCache.EndsWith("\\", StringComparison.Ordinal)
|
{
|
||||||
? CachePrefix + "\\"
|
chosenPrefixed = cachePrefixed;
|
||||||
: CachePrefix;
|
chosenLength = cacheMatch;
|
||||||
normalized = normalized.Replace(normalizedCache, replacement, StringComparison.OrdinalIgnoreCase);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NormalizePrefixedPathKey(normalized);
|
return NormalizePrefixedPathKey(chosenPrefixed ?? normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
public FileCacheEntity? CreateCacheEntry(string path)
|
public FileCacheEntity? CreateCacheEntry(string path)
|
||||||
@@ -94,7 +151,9 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
FileInfo fi = new(path);
|
FileInfo fi = new(path);
|
||||||
if (!fi.Exists) return null;
|
if (!fi.Exists) return null;
|
||||||
_logger.LogTrace("Creating cache entry for {path}", path);
|
_logger.LogTrace("Creating cache entry for {path}", path);
|
||||||
return CreateFileEntity(_configService.Current.CacheFolder.ToLowerInvariant(), CachePrefix, fi);
|
var cacheFolder = _configService.Current.CacheFolder;
|
||||||
|
if (string.IsNullOrEmpty(cacheFolder)) return null;
|
||||||
|
return CreateFileEntity(cacheFolder, CachePrefix, fi);
|
||||||
}
|
}
|
||||||
|
|
||||||
public FileCacheEntity? CreateFileEntry(string path)
|
public FileCacheEntity? CreateFileEntry(string path)
|
||||||
@@ -102,14 +161,18 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
FileInfo fi = new(path);
|
FileInfo fi = new(path);
|
||||||
if (!fi.Exists) return null;
|
if (!fi.Exists) return null;
|
||||||
_logger.LogTrace("Creating file entry for {path}", path);
|
_logger.LogTrace("Creating file entry for {path}", path);
|
||||||
return CreateFileEntity(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), PenumbraPrefix, fi);
|
var modDirectory = _ipcManager.Penumbra.ModDirectory;
|
||||||
|
if (string.IsNullOrEmpty(modDirectory)) return null;
|
||||||
|
return CreateFileEntity(modDirectory, PenumbraPrefix, fi);
|
||||||
}
|
}
|
||||||
|
|
||||||
private FileCacheEntity? CreateFileEntity(string directory, string prefix, FileInfo fi)
|
private FileCacheEntity? CreateFileEntity(string directory, string prefix, FileInfo fi)
|
||||||
{
|
{
|
||||||
var fullName = fi.FullName.ToLowerInvariant();
|
if (!TryBuildPrefixedPath(fi.FullName, directory, prefix, out var prefixedPath, out _))
|
||||||
if (!fullName.Contains(directory, StringComparison.Ordinal)) return null;
|
{
|
||||||
string prefixedPath = fullName.Replace(directory, prefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal);
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return CreateFileCacheEntity(fi, prefixedPath);
|
return CreateFileCacheEntity(fi, prefixedPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,6 +430,7 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
lock (_fileWriteLock)
|
lock (_fileWriteLock)
|
||||||
{
|
{
|
||||||
StringBuilder sb = new();
|
StringBuilder sb = new();
|
||||||
|
sb.AppendLine(BuildVersionHeader());
|
||||||
foreach (var entry in _fileCaches.Values.SelectMany(k => k.Values).OrderBy(f => f.PrefixedFilePath, StringComparer.OrdinalIgnoreCase))
|
foreach (var entry in _fileCaches.Values.SelectMany(k => k.Values).OrderBy(f => f.PrefixedFilePath, StringComparer.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
sb.AppendLine(entry.CsvEntry);
|
sb.AppendLine(entry.CsvEntry);
|
||||||
@@ -389,6 +453,66 @@ 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)
|
||||||
|
{
|
||||||
|
_csvHeaderEnsured = true;
|
||||||
|
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());
|
||||||
|
_csvHeaderEnsured = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureCsvHeaderLockedCached()
|
||||||
|
{
|
||||||
|
if (_csvHeaderEnsured)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureCsvHeaderLocked();
|
||||||
|
_csvHeaderEnsured = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BackupUnsupportedCache(string suffix)
|
||||||
|
{
|
||||||
|
var sanitizedSuffix = string.IsNullOrWhiteSpace(suffix) ? "unsupported" : $"{suffix}.unsupported";
|
||||||
|
var backupPath = _csvPath + "." + sanitizedSuffix;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Move(_csvPath, backupPath, overwrite: true);
|
||||||
|
_logger.LogWarning("Backed up unsupported file cache to {path}", backupPath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to back up unsupported file cache to {path}", backupPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal FileCacheEntity MigrateFileHashToExtension(FileCacheEntity fileCache, string ext)
|
internal FileCacheEntity MigrateFileHashToExtension(FileCacheEntity fileCache, string ext)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -427,7 +551,16 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
AddHashedFile(entity);
|
AddHashedFile(entity);
|
||||||
lock (_fileWriteLock)
|
lock (_fileWriteLock)
|
||||||
{
|
{
|
||||||
File.AppendAllLines(_csvPath, new[] { entity.CsvEntry });
|
if (!File.Exists(_csvPath))
|
||||||
|
{
|
||||||
|
File.WriteAllLines(_csvPath, new[] { BuildVersionHeader(), entity.CsvEntry });
|
||||||
|
_csvHeaderEnsured = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EnsureCsvHeaderLockedCached();
|
||||||
|
File.AppendAllLines(_csvPath, new[] { entity.CsvEntry });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var result = GetFileCacheByPath(fileInfo.FullName);
|
var result = GetFileCacheByPath(fileInfo.FullName);
|
||||||
_logger.LogTrace("Creating cache entity for {name} success: {success}", fileInfo.FullName, (result != null));
|
_logger.LogTrace("Creating cache entity for {name} success: {success}", fileInfo.FullName, (result != null));
|
||||||
@@ -546,49 +679,111 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
_logger.LogWarning("Could not load entries from {path}, continuing with empty file cache", _csvPath);
|
_logger.LogWarning("Could not load entries from {path}, continuing with empty file cache", _csvPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Found {amount} files in {path}", entries.Length, _csvPath);
|
bool rewriteRequired = false;
|
||||||
|
bool parseEntries = entries.Length > 0;
|
||||||
|
int startIndex = 0;
|
||||||
|
|
||||||
Dictionary<string, bool> processedFiles = new(StringComparer.OrdinalIgnoreCase);
|
if (entries.Length > 0)
|
||||||
foreach (var entry in entries)
|
|
||||||
{
|
{
|
||||||
var splittedEntry = entry.Split(CsvSplit, StringSplitOptions.None);
|
var headerLine = entries[0];
|
||||||
try
|
var hasHeader = !string.IsNullOrEmpty(headerLine) &&
|
||||||
|
headerLine.StartsWith(FileCacheVersionHeaderPrefix, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (hasHeader)
|
||||||
{
|
{
|
||||||
var hash = splittedEntry[0];
|
if (!TryParseVersionHeader(headerLine, out var parsedVersion))
|
||||||
if (hash.Length != 40) throw new InvalidOperationException("Expected Hash length of 40, received " + hash.Length);
|
|
||||||
var path = splittedEntry[1];
|
|
||||||
var time = splittedEntry[2];
|
|
||||||
|
|
||||||
if (processedFiles.ContainsKey(path))
|
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Already processed {file}, ignoring", path);
|
_logger.LogWarning("Failed to parse file cache version header \"{header}\". Backing up existing cache.", headerLine);
|
||||||
continue;
|
BackupUnsupportedCache("invalid-version");
|
||||||
|
parseEntries = false;
|
||||||
|
rewriteRequired = true;
|
||||||
|
entries = Array.Empty<string>();
|
||||||
}
|
}
|
||||||
|
else if (parsedVersion != FileCacheVersion)
|
||||||
processedFiles.Add(path, value: true);
|
|
||||||
|
|
||||||
long size = -1;
|
|
||||||
long compressed = -1;
|
|
||||||
if (splittedEntry.Length > 3)
|
|
||||||
{
|
{
|
||||||
if (long.TryParse(splittedEntry[3], CultureInfo.InvariantCulture, out long result))
|
_logger.LogWarning("Unsupported file cache version {version} detected (expected {expected}). Backing up existing cache.", parsedVersion, FileCacheVersion);
|
||||||
{
|
BackupUnsupportedCache($"v{parsedVersion}");
|
||||||
size = result;
|
parseEntries = false;
|
||||||
}
|
rewriteRequired = true;
|
||||||
if (long.TryParse(splittedEntry[4], CultureInfo.InvariantCulture, out long resultCompressed))
|
entries = Array.Empty<string>();
|
||||||
{
|
}
|
||||||
compressed = resultCompressed;
|
else
|
||||||
}
|
{
|
||||||
|
startIndex = 1;
|
||||||
}
|
}
|
||||||
AddHashedFile(ReplacePathPrefixes(new FileCacheEntity(hash, path, time, size, compressed)));
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
else if (entries.Length > 0)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Failed to initialize entry {entry}, ignoring", entry);
|
_logger.LogInformation("File cache missing version header, scheduling rewrite.");
|
||||||
|
rewriteRequired = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (processedFiles.Count != entries.Length)
|
var totalEntries = Math.Max(0, entries.Length - startIndex);
|
||||||
|
Dictionary<string, bool> processedFiles = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (parseEntries && totalEntries > 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Found {amount} files in {path}", totalEntries, _csvPath);
|
||||||
|
|
||||||
|
for (var index = startIndex; index < entries.Length; index++)
|
||||||
|
{
|
||||||
|
var entry = entries[index];
|
||||||
|
if (string.IsNullOrWhiteSpace(entry))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var splittedEntry = entry.Split(CsvSplit, StringSplitOptions.None);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var hash = splittedEntry[0];
|
||||||
|
if (hash.Length != 40)
|
||||||
|
throw new InvalidOperationException("Expected Hash length of 40, received " + hash.Length);
|
||||||
|
var path = splittedEntry[1];
|
||||||
|
var time = splittedEntry[2];
|
||||||
|
|
||||||
|
if (processedFiles.ContainsKey(path))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Already processed {file}, ignoring", path);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
processedFiles.Add(path, value: true);
|
||||||
|
|
||||||
|
long size = -1;
|
||||||
|
long compressed = -1;
|
||||||
|
if (splittedEntry.Length > 3)
|
||||||
|
{
|
||||||
|
if (long.TryParse(splittedEntry[3], CultureInfo.InvariantCulture, out long result))
|
||||||
|
{
|
||||||
|
size = result;
|
||||||
|
}
|
||||||
|
if (splittedEntry.Length > 4 &&
|
||||||
|
long.TryParse(splittedEntry[4], CultureInfo.InvariantCulture, out long resultCompressed))
|
||||||
|
{
|
||||||
|
compressed = resultCompressed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AddHashedFile(ReplacePathPrefixes(new FileCacheEntity(hash, path, time, size, compressed)));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to initialize entry {entry}, ignoring", entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processedFiles.Count != totalEntries)
|
||||||
|
{
|
||||||
|
rewriteRequired = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!parseEntries && entries.Length > 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Skipping existing file cache entries due to incompatible version.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rewriteRequired)
|
||||||
{
|
{
|
||||||
WriteOutFullCsv();
|
WriteOutFullCsv();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,25 +2,33 @@
|
|||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace LightlessSync.FileCache;
|
namespace LightlessSync.FileCache;
|
||||||
|
|
||||||
public sealed class FileCompactor
|
public sealed class FileCompactor : IDisposable
|
||||||
{
|
{
|
||||||
public const uint FSCTL_DELETE_EXTERNAL_BACKING = 0x90314U;
|
public const uint FSCTL_DELETE_EXTERNAL_BACKING = 0x90314U;
|
||||||
public const ulong WOF_PROVIDER_FILE = 2UL;
|
public const ulong WOF_PROVIDER_FILE = 2UL;
|
||||||
|
|
||||||
private readonly Dictionary<string, int> _clusterSizes;
|
private readonly Dictionary<string, int> _clusterSizes;
|
||||||
|
private readonly ConcurrentDictionary<string, byte> _pendingCompactions;
|
||||||
private readonly WOF_FILE_COMPRESSION_INFO_V1 _efInfo;
|
private readonly WOF_FILE_COMPRESSION_INFO_V1 _efInfo;
|
||||||
private readonly ILogger<FileCompactor> _logger;
|
private readonly ILogger<FileCompactor> _logger;
|
||||||
|
|
||||||
private readonly LightlessConfigService _lightlessConfigService;
|
private readonly LightlessConfigService _lightlessConfigService;
|
||||||
private readonly DalamudUtilService _dalamudUtilService;
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
|
private readonly Channel<string> _compactionQueue;
|
||||||
|
private readonly CancellationTokenSource _compactionCts = new();
|
||||||
|
private readonly Task _compactionWorker;
|
||||||
|
|
||||||
public FileCompactor(ILogger<FileCompactor> logger, LightlessConfigService lightlessConfigService, DalamudUtilService dalamudUtilService)
|
public FileCompactor(ILogger<FileCompactor> logger, LightlessConfigService lightlessConfigService, DalamudUtilService dalamudUtilService)
|
||||||
{
|
{
|
||||||
_clusterSizes = new(StringComparer.Ordinal);
|
_clusterSizes = new(StringComparer.Ordinal);
|
||||||
|
_pendingCompactions = new(StringComparer.OrdinalIgnoreCase);
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_lightlessConfigService = lightlessConfigService;
|
_lightlessConfigService = lightlessConfigService;
|
||||||
_dalamudUtilService = dalamudUtilService;
|
_dalamudUtilService = dalamudUtilService;
|
||||||
@@ -29,6 +37,18 @@ public sealed class FileCompactor
|
|||||||
Algorithm = CompressionAlgorithm.XPRESS8K,
|
Algorithm = CompressionAlgorithm.XPRESS8K,
|
||||||
Flags = 0
|
Flags = 0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_compactionQueue = Channel.CreateUnbounded<string>(new UnboundedChannelOptions
|
||||||
|
{
|
||||||
|
SingleReader = true,
|
||||||
|
SingleWriter = false
|
||||||
|
});
|
||||||
|
_compactionWorker = Task.Factory.StartNew(
|
||||||
|
() => ProcessQueueAsync(_compactionCts.Token),
|
||||||
|
_compactionCts.Token,
|
||||||
|
TaskCreationOptions.LongRunning,
|
||||||
|
TaskScheduler.Default)
|
||||||
|
.Unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum CompressionAlgorithm
|
private enum CompressionAlgorithm
|
||||||
@@ -87,7 +107,30 @@ public sealed class FileCompactor
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
CompactFile(filePath);
|
EnqueueCompaction(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_compactionQueue.Writer.TryComplete();
|
||||||
|
_compactionCts.Cancel();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_compactionWorker.Wait(TimeSpan.FromSeconds(5)))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Compaction worker did not shut down within timeout");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Error shutting down compaction worker");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_compactionCts.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
[DllImport("kernel32.dll")]
|
[DllImport("kernel32.dll")]
|
||||||
@@ -226,4 +269,67 @@ public sealed class FileCompactor
|
|||||||
public CompressionAlgorithm Algorithm;
|
public CompressionAlgorithm Algorithm;
|
||||||
public ulong Flags;
|
public ulong Flags;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private void EnqueueCompaction(string filePath)
|
||||||
|
{
|
||||||
|
if (!_pendingCompactions.TryAdd(filePath, 0))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_compactionQueue.Writer.TryWrite(filePath))
|
||||||
|
{
|
||||||
|
_pendingCompactions.TryRemove(filePath, out _);
|
||||||
|
_logger.LogDebug("Failed to enqueue compaction job for {file}", filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessQueueAsync(CancellationToken token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (await _compactionQueue.Reader.WaitToReadAsync(token).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
while (_compactionQueue.Reader.TryRead(out var filePath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_dalamudUtilService.IsWine || !_lightlessConfigService.Current.UseCompactor)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Skipping compaction for missing file {file}", filePath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
CompactFile(filePath);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Error compacting file {file}", filePath);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_pendingCompactions.TryRemove(filePath, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// expected during shutdown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||||
|
|
||||||
|
public enum LightfinderDtrDisplayMode
|
||||||
|
{
|
||||||
|
NearbyBroadcasts = 0,
|
||||||
|
PendingPairRequests = 1,
|
||||||
|
}
|
||||||
@@ -22,6 +22,13 @@ public class LightlessConfig : ILightlessConfiguration
|
|||||||
public DtrEntry.Colors DtrColorsDefault { get; set; } = default;
|
public DtrEntry.Colors DtrColorsDefault { get; set; } = default;
|
||||||
public DtrEntry.Colors DtrColorsNotConnected { get; set; } = new(Glow: 0x0428FFu);
|
public DtrEntry.Colors DtrColorsNotConnected { get; set; } = new(Glow: 0x0428FFu);
|
||||||
public DtrEntry.Colors DtrColorsPairsInRange { get; set; } = new(Glow: 0xFFBA47u);
|
public DtrEntry.Colors DtrColorsPairsInRange { get; set; } = new(Glow: 0xFFBA47u);
|
||||||
|
public bool ShowLightfinderInDtr { get; set; } = false;
|
||||||
|
public bool UseLightfinderColorsInDtr { get; set; } = true;
|
||||||
|
public DtrEntry.Colors DtrColorsLightfinderEnabled { get; set; } = new(Foreground: 0xB590FFu, Glow: 0x4F406Eu);
|
||||||
|
public DtrEntry.Colors DtrColorsLightfinderDisabled { get; set; } = new(Foreground: 0xD44444u, Glow: 0x642222u);
|
||||||
|
public DtrEntry.Colors DtrColorsLightfinderCooldown { get; set; } = new(Foreground: 0xFFE97Au, Glow: 0x766C3Au);
|
||||||
|
public DtrEntry.Colors DtrColorsLightfinderUnavailable { get; set; } = new(Foreground: 0x000000u, Glow: 0x000000u);
|
||||||
|
public LightfinderDtrDisplayMode LightfinderDtrDisplayMode { get; set; } = LightfinderDtrDisplayMode.PendingPairRequests;
|
||||||
public bool UseLightlessRedesign { get; set; } = true;
|
public bool UseLightlessRedesign { get; set; } = true;
|
||||||
public bool EnableRightClickMenus { get; set; } = true;
|
public bool EnableRightClickMenus { get; set; } = true;
|
||||||
public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both;
|
public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both;
|
||||||
@@ -56,9 +63,11 @@ 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;
|
||||||
|
public bool EnableDirectDownloads { get; set; } = true;
|
||||||
public int TimeSpanBetweenScansInSeconds { get; set; } = 30;
|
public int TimeSpanBetweenScansInSeconds { get; set; } = 30;
|
||||||
public int TransferBarsHeight { get; set; } = 12;
|
public int TransferBarsHeight { get; set; } = 12;
|
||||||
public bool TransferBarsShowText { get; set; } = true;
|
public bool TransferBarsShowText { get; set; } = true;
|
||||||
@@ -69,17 +78,68 @@ public class LightlessConfig : ILightlessConfiguration
|
|||||||
public bool AutoPopulateEmptyNotesFromCharaName { get; set; } = false;
|
public bool AutoPopulateEmptyNotesFromCharaName { get; set; } = false;
|
||||||
public int Version { get; set; } = 1;
|
public int Version { get; set; } = 1;
|
||||||
public NotificationLocation WarningNotification { get; set; } = NotificationLocation.Both;
|
public NotificationLocation WarningNotification { get; set; } = NotificationLocation.Both;
|
||||||
|
|
||||||
|
// Lightless Notification Configuration
|
||||||
|
public bool UseLightlessNotifications { get; set; } = true;
|
||||||
|
public bool ShowNotificationProgress { get; set; } = true;
|
||||||
|
public NotificationLocation LightlessInfoNotification { get; set; } = NotificationLocation.LightlessUi;
|
||||||
|
public NotificationLocation LightlessWarningNotification { get; set; } = NotificationLocation.LightlessUi;
|
||||||
|
public NotificationLocation LightlessErrorNotification { get; set; } = NotificationLocation.ChatAndLightlessUi;
|
||||||
|
public NotificationLocation LightlessPairRequestNotification { get; set; } = NotificationLocation.LightlessUi;
|
||||||
|
public NotificationLocation LightlessDownloadNotification { get; set; } = NotificationLocation.TextOverlay;
|
||||||
|
public NotificationLocation LightlessPerformanceNotification { get; set; } = NotificationLocation.LightlessUi;
|
||||||
|
|
||||||
|
// Basic Settings
|
||||||
|
public float NotificationOpacity { get; set; } = 0.95f;
|
||||||
|
public int MaxSimultaneousNotifications { get; set; } = 5;
|
||||||
|
public bool AutoDismissOnAction { get; set; } = true;
|
||||||
|
public bool DismissNotificationOnClick { get; set; } = false;
|
||||||
|
public bool ShowNotificationTimestamp { get; set; } = false;
|
||||||
|
|
||||||
|
// Position & Layout
|
||||||
|
public NotificationCorner NotificationCorner { get; set; } = NotificationCorner.Right;
|
||||||
|
public int NotificationOffsetY { get; set; } = 50;
|
||||||
|
public int NotificationOffsetX { get; set; } = 0;
|
||||||
|
public float NotificationWidth { get; set; } = 350f;
|
||||||
|
public float NotificationSpacing { get; set; } = 8f;
|
||||||
|
|
||||||
|
// Animation & Effects
|
||||||
|
public float NotificationAnimationSpeed { get; set; } = 10f;
|
||||||
|
public float NotificationSlideSpeed { get; set; } = 10f;
|
||||||
|
public float NotificationAccentBarWidth { get; set; } = 3f;
|
||||||
|
|
||||||
|
// Duration per Type
|
||||||
|
public int InfoNotificationDurationSeconds { get; set; } = 10;
|
||||||
|
public int WarningNotificationDurationSeconds { get; set; } = 15;
|
||||||
|
public int ErrorNotificationDurationSeconds { get; set; } = 20;
|
||||||
|
public int PairRequestDurationSeconds { get; set; } = 180;
|
||||||
|
public int DownloadNotificationDurationSeconds { get; set; } = 300;
|
||||||
|
public int PerformanceNotificationDurationSeconds { get; set; } = 20;
|
||||||
|
public uint CustomInfoSoundId { get; set; } = 2; // Se2
|
||||||
|
public uint CustomWarningSoundId { get; set; } = 16; // Se15
|
||||||
|
public uint CustomErrorSoundId { get; set; } = 16; // Se15
|
||||||
|
public uint PairRequestSoundId { get; set; } = 5; // Se5
|
||||||
|
public uint PerformanceSoundId { get; set; } = 16; // Se15
|
||||||
|
public bool DisableInfoSound { get; set; } = true;
|
||||||
|
public bool DisableWarningSound { get; set; } = true;
|
||||||
|
public bool DisableErrorSound { get; set; } = true;
|
||||||
|
public bool DisablePairRequestSound { get; set; } = true;
|
||||||
|
public bool DisablePerformanceSound { get; set; } = true;
|
||||||
|
public bool ShowPerformanceNotificationActions { get; set; } = true;
|
||||||
|
public bool ShowPairRequestNotificationActions { get; set; } = true;
|
||||||
public bool UseFocusTarget { get; set; } = false;
|
public bool UseFocusTarget { get; set; } = false;
|
||||||
public bool overrideFriendColor { get; set; } = false;
|
public bool overrideFriendColor { get; set; } = false;
|
||||||
public bool overridePartyColor { get; set; } = false;
|
public bool overridePartyColor { get; set; } = false;
|
||||||
public bool overrideFcTagColor { get; set; } = false;
|
public bool overrideFcTagColor { get; set; } = false;
|
||||||
public bool useColoredUIDs { get; set; } = true;
|
public bool useColoredUIDs { get; set; } = true;
|
||||||
public bool BroadcastEnabled { get; set; } = false;
|
public bool BroadcastEnabled { get; set; } = false;
|
||||||
|
public bool LightfinderAutoEnableOnConnect { get; set; } = false;
|
||||||
public short LightfinderLabelOffsetX { get; set; } = 0;
|
public short LightfinderLabelOffsetX { get; set; } = 0;
|
||||||
public short LightfinderLabelOffsetY { get; set; } = 0;
|
public short LightfinderLabelOffsetY { get; set; } = 0;
|
||||||
public bool LightfinderLabelUseIcon { get; set; } = false;
|
public bool LightfinderLabelUseIcon { get; set; } = false;
|
||||||
public bool LightfinderLabelShowOwn { get; set; } = true;
|
public bool LightfinderLabelShowOwn { get; set; } = true;
|
||||||
public bool LightfinderLabelShowPaired { get; set; } = true;
|
public bool LightfinderLabelShowPaired { get; set; } = true;
|
||||||
|
public bool LightfinderLabelShowHidden { get; set; } = false;
|
||||||
public string LightfinderLabelIconGlyph { get; set; } = SeIconCharExtensions.ToIconString(SeIconChar.Hyadelyn);
|
public string LightfinderLabelIconGlyph { get; set; } = SeIconCharExtensions.ToIconString(SeIconChar.Hyadelyn);
|
||||||
public float LightfinderLabelScale { get; set; } = 1.0f;
|
public float LightfinderLabelScale { get; set; } = 1.0f;
|
||||||
public bool LightfinderAutoAlign { get; set; } = true;
|
public bool LightfinderAutoAlign { get; set; } = true;
|
||||||
@@ -87,4 +147,5 @@ public class LightlessConfig : ILightlessConfiguration
|
|||||||
public DateTime BroadcastTtl { get; set; } = DateTime.MinValue;
|
public DateTime BroadcastTtl { get; set; } = DateTime.MinValue;
|
||||||
public bool SyncshellFinderEnabled { get; set; } = false;
|
public bool SyncshellFinderEnabled { get; set; } = false;
|
||||||
public string? SelectedFinderSyncshell { get; set; } = null;
|
public string? SelectedFinderSyncshell { get; set; } = null;
|
||||||
|
public string LastSeenVersion { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class UiStyleOverride
|
||||||
|
{
|
||||||
|
public uint? Color { get; set; }
|
||||||
|
public float? Float { get; set; }
|
||||||
|
public Vector2Config? Vector2 { get; set; }
|
||||||
|
|
||||||
|
public bool IsEmpty => Color is null && Float is null && Vector2 is null;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public record struct Vector2Config(float X, float Y)
|
||||||
|
{
|
||||||
|
public static implicit operator Vector2(Vector2Config value) => new(value.X, value.Y);
|
||||||
|
public static implicit operator Vector2Config(Vector2 value) => new(value.X, value.Y);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class UiThemeConfig : ILightlessConfiguration
|
||||||
|
{
|
||||||
|
public Dictionary<string, UiStyleOverride> StyleOverrides { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public int Version { get; set; } = 1;
|
||||||
|
}
|
||||||
@@ -1,16 +1,28 @@
|
|||||||
namespace LightlessSync.LightlessConfiguration.Models;
|
namespace LightlessSync.LightlessConfiguration.Models;
|
||||||
|
|
||||||
public enum NotificationLocation
|
public enum NotificationLocation
|
||||||
{
|
{
|
||||||
Nowhere,
|
Nowhere,
|
||||||
Chat,
|
Chat,
|
||||||
Toast,
|
Toast,
|
||||||
Both
|
Both,
|
||||||
|
LightlessUi,
|
||||||
|
ChatAndLightlessUi,
|
||||||
|
TextOverlay,
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum NotificationType
|
public enum NotificationType
|
||||||
{
|
{
|
||||||
Info,
|
Info,
|
||||||
Warning,
|
Warning,
|
||||||
Error
|
Error,
|
||||||
|
PairRequest,
|
||||||
|
Download,
|
||||||
|
Performance
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum NotificationCorner
|
||||||
|
{
|
||||||
|
Right,
|
||||||
|
Left
|
||||||
}
|
}
|
||||||
14
LightlessSync/LightlessConfiguration/UiThemeConfigService.cs
Normal file
14
LightlessSync/LightlessConfiguration/UiThemeConfigService.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using LightlessSync.LightlessConfiguration.Configurations;
|
||||||
|
|
||||||
|
namespace LightlessSync.LightlessConfiguration;
|
||||||
|
|
||||||
|
public class UiThemeConfigService : ConfigurationServiceBase<UiThemeConfig>
|
||||||
|
{
|
||||||
|
public const string ConfigName = "ui-theme.json";
|
||||||
|
|
||||||
|
public UiThemeConfigService(string configDir) : base(configDir)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ConfigurationName => ConfigName;
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ using LightlessSync.UI;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Serilog;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
namespace LightlessSync;
|
namespace LightlessSync;
|
||||||
@@ -101,7 +102,7 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
|
|||||||
|
|
||||||
UIColors.Initialize(_lightlessConfigService);
|
UIColors.Initialize(_lightlessConfigService);
|
||||||
Mediator.StartQueueProcessing();
|
Mediator.StartQueueProcessing();
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +116,24 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
|
|||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CheckVersion()
|
||||||
|
{
|
||||||
|
var ver = Assembly.GetExecutingAssembly().GetName().Version;
|
||||||
|
var currentVersion = ver == null ? string.Empty : $"{ver.Major}.{ver.Minor}.{ver.Build}";
|
||||||
|
var lastSeen = _lightlessConfigService.Current.LastSeenVersion ?? string.Empty;
|
||||||
|
Logger.LogInformation("Last seen version: {lastSeen}, current version: {currentVersion}", lastSeen, currentVersion);
|
||||||
|
Logger.LogInformation("User has valid setup: {hasValidSetup}", _lightlessConfigService.Current.HasValidSetup());
|
||||||
|
Logger.LogInformation("Server has valid config: {hasValidConfig}", _serverConfigurationManager.HasValidConfig());
|
||||||
|
// Show update notes if version has changed and user has valid setup
|
||||||
|
|
||||||
|
if (!string.Equals(lastSeen, currentVersion, StringComparison.Ordinal) &&
|
||||||
|
_lightlessConfigService.Current.HasValidSetup() &&
|
||||||
|
_serverConfigurationManager.HasValidConfig())
|
||||||
|
{
|
||||||
|
Mediator.Publish(new UiToggleMessage(typeof(UpdateNotesUi)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void DalamudUtilOnLogIn()
|
private void DalamudUtilOnLogIn()
|
||||||
{
|
{
|
||||||
@@ -154,6 +173,7 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
|
|||||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<VisibleUserDataDistributor>();
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<VisibleUserDataDistributor>();
|
||||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<NotificationService>();
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<NotificationService>();
|
||||||
_runtimeServiceScope.ServiceProvider.GetRequiredService<NameplateService>();
|
_runtimeServiceScope.ServiceProvider.GetRequiredService<NameplateService>();
|
||||||
|
CheckVersion();
|
||||||
|
|
||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
if (_lightlessConfigService.Current.LogLevel != LogLevel.Information)
|
if (_lightlessConfigService.Current.LogLevel != LogLevel.Information)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project Sdk="Dalamud.NET.Sdk/13.1.0">
|
<Project Sdk="Dalamud.NET.Sdk/13.1.0">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors></Authors>
|
<Authors></Authors>
|
||||||
<Company></Company>
|
<Company></Company>
|
||||||
<Version>1.12.1</Version>
|
<Version>1.12.2.7</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>
|
||||||
@@ -46,6 +46,7 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.7.0" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.7.0" />
|
||||||
|
<PackageReference Include="YamlDotNet" Version="16.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
@@ -64,6 +65,8 @@
|
|||||||
<None Update="images\icon.png">
|
<None Update="images\icon.png">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
<EmbeddedResource Include="Changelog\changelog.yaml" />
|
||||||
|
<EmbeddedResource Include="Changelog\credits.yaml" />
|
||||||
<EmbeddedResource Include="Localization\de.json" />
|
<EmbeddedResource Include="Localization\de.json" />
|
||||||
<EmbeddedResource Include="Localization\fr.json" />
|
<EmbeddedResource Include="Localization\fr.json" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
|
using LightlessSync.LightlessConfiguration;
|
||||||
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.WebAPI.Files;
|
using LightlessSync.WebAPI.Files;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -10,21 +12,38 @@ public class FileDownloadManagerFactory
|
|||||||
private readonly FileCacheManager _fileCacheManager;
|
private readonly FileCacheManager _fileCacheManager;
|
||||||
private readonly FileCompactor _fileCompactor;
|
private readonly FileCompactor _fileCompactor;
|
||||||
private readonly FileTransferOrchestrator _fileTransferOrchestrator;
|
private readonly FileTransferOrchestrator _fileTransferOrchestrator;
|
||||||
|
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
private readonly LightlessMediator _lightlessMediator;
|
private readonly LightlessMediator _lightlessMediator;
|
||||||
|
private readonly LightlessConfigService _configService;
|
||||||
|
|
||||||
public FileDownloadManagerFactory(ILoggerFactory loggerFactory, LightlessMediator lightlessMediator, FileTransferOrchestrator fileTransferOrchestrator,
|
public FileDownloadManagerFactory(
|
||||||
FileCacheManager fileCacheManager, FileCompactor fileCompactor)
|
ILoggerFactory loggerFactory,
|
||||||
|
LightlessMediator lightlessMediator,
|
||||||
|
FileTransferOrchestrator fileTransferOrchestrator,
|
||||||
|
FileCacheManager fileCacheManager,
|
||||||
|
FileCompactor fileCompactor,
|
||||||
|
PairProcessingLimiter pairProcessingLimiter,
|
||||||
|
LightlessConfigService configService)
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
_lightlessMediator = lightlessMediator;
|
_lightlessMediator = lightlessMediator;
|
||||||
_fileTransferOrchestrator = fileTransferOrchestrator;
|
_fileTransferOrchestrator = fileTransferOrchestrator;
|
||||||
_fileCacheManager = fileCacheManager;
|
_fileCacheManager = fileCacheManager;
|
||||||
_fileCompactor = fileCompactor;
|
_fileCompactor = fileCompactor;
|
||||||
|
_pairProcessingLimiter = pairProcessingLimiter;
|
||||||
|
_configService = configService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public FileDownloadManager Create()
|
public FileDownloadManager Create()
|
||||||
{
|
{
|
||||||
return new FileDownloadManager(_loggerFactory.CreateLogger<FileDownloadManager>(), _lightlessMediator, _fileTransferOrchestrator, _fileCacheManager, _fileCompactor);
|
return new FileDownloadManager(
|
||||||
|
_loggerFactory.CreateLogger<FileDownloadManager>(),
|
||||||
|
_lightlessMediator,
|
||||||
|
_fileTransferOrchestrator,
|
||||||
|
_fileCacheManager,
|
||||||
|
_fileCompactor,
|
||||||
|
_pairProcessingLimiter,
|
||||||
|
_configService);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,7 +98,19 @@ public class PlayerDataFactory
|
|||||||
|
|
||||||
private unsafe bool CheckForNullDrawObjectUnsafe(IntPtr playerPointer)
|
private unsafe bool CheckForNullDrawObjectUnsafe(IntPtr playerPointer)
|
||||||
{
|
{
|
||||||
return ((Character*)playerPointer)->GameObject.DrawObject == null;
|
if (playerPointer == IntPtr.Zero)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var character = (Character*)playerPointer;
|
||||||
|
|
||||||
|
if (character == null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var gameObject = &character->GameObject;
|
||||||
|
if (gameObject == null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return gameObject->DrawObject == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<CharacterDataFragment> CreateCharacterData(GameObjectHandler playerRelatedObject, CancellationToken ct)
|
private async Task<CharacterDataFragment> CreateCharacterData(GameObjectHandler playerRelatedObject, CancellationToken ct)
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (_allClientPairs.TryGetValue(user, out var pair))
|
if (_allClientPairs.TryGetValue(user, out var pair))
|
||||||
{
|
{
|
||||||
Mediator.Publish(new ClearProfileDataMessage(pair.UserData));
|
Mediator.Publish(new ClearProfileUserDataMessage(pair.UserData));
|
||||||
pair.MarkOffline();
|
pair.MarkOffline();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (!_allClientPairs.ContainsKey(dto.User)) throw new InvalidOperationException("No user found for " + dto);
|
if (!_allClientPairs.ContainsKey(dto.User)) throw new InvalidOperationException("No user found for " + dto);
|
||||||
|
|
||||||
Mediator.Publish(new ClearProfileDataMessage(dto.User));
|
Mediator.Publish(new ClearProfileUserDataMessage(dto.User));
|
||||||
|
|
||||||
var pair = _allClientPairs[dto.User];
|
var pair = _allClientPairs[dto.User];
|
||||||
if (pair.HasCachedPlayer)
|
if (pair.HasCachedPlayer)
|
||||||
@@ -254,7 +254,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
if (pair.UserPair.OtherPermissions.IsPaused() != dto.Permissions.IsPaused())
|
if (pair.UserPair.OtherPermissions.IsPaused() != dto.Permissions.IsPaused())
|
||||||
{
|
{
|
||||||
Mediator.Publish(new ClearProfileDataMessage(dto.User));
|
Mediator.Publish(new ClearProfileUserDataMessage(dto.User));
|
||||||
}
|
}
|
||||||
|
|
||||||
pair.UserPair.OtherPermissions = dto.Permissions;
|
pair.UserPair.OtherPermissions = dto.Permissions;
|
||||||
@@ -280,7 +280,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
if (pair.UserPair.OwnPermissions.IsPaused() != dto.Permissions.IsPaused())
|
if (pair.UserPair.OwnPermissions.IsPaused() != dto.Permissions.IsPaused())
|
||||||
{
|
{
|
||||||
Mediator.Publish(new ClearProfileDataMessage(dto.User));
|
Mediator.Publish(new ClearProfileUserDataMessage(dto.User));
|
||||||
}
|
}
|
||||||
|
|
||||||
pair.UserPair.OwnPermissions = dto.Permissions;
|
pair.UserPair.OwnPermissions = dto.Permissions;
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton<PluginWarningNotificationService>();
|
collection.AddSingleton<PluginWarningNotificationService>();
|
||||||
collection.AddSingleton<FileCompactor>();
|
collection.AddSingleton<FileCompactor>();
|
||||||
collection.AddSingleton<TagHandler>();
|
collection.AddSingleton<TagHandler>();
|
||||||
|
collection.AddSingleton(s => new Lazy<ApiController>(() => s.GetRequiredService<ApiController>()));
|
||||||
collection.AddSingleton<PairRequestService>();
|
collection.AddSingleton<PairRequestService>();
|
||||||
collection.AddSingleton<IdDisplayHandler>();
|
collection.AddSingleton<IdDisplayHandler>();
|
||||||
collection.AddSingleton<PlayerPerformanceService>();
|
collection.AddSingleton<PlayerPerformanceService>();
|
||||||
@@ -142,8 +143,18 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
clientState, objectTable, framework, gameGui, condition, gameData, targetManager, gameConfig,
|
clientState, objectTable, framework, gameGui, condition, gameData, targetManager, gameConfig,
|
||||||
s.GetRequiredService<BlockedCharacterHandler>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(),
|
s.GetRequiredService<BlockedCharacterHandler>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(),
|
||||||
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<PlayerPerformanceConfigService>()));
|
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<PlayerPerformanceConfigService>()));
|
||||||
collection.AddSingleton((s) => new DtrEntry(s.GetRequiredService<ILogger<DtrEntry>>(), dtrBar, s.GetRequiredService<LightlessConfigService>(),
|
collection.AddSingleton((s) => new DtrEntry(
|
||||||
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PairManager>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<ServerConfigurationManager>()));
|
s.GetRequiredService<ILogger<DtrEntry>>(),
|
||||||
|
dtrBar,
|
||||||
|
s.GetRequiredService<LightlessConfigService>(),
|
||||||
|
s.GetRequiredService<LightlessMediator>(),
|
||||||
|
s.GetRequiredService<PairManager>(),
|
||||||
|
s.GetRequiredService<PairRequestService>(),
|
||||||
|
s.GetRequiredService<ApiController>(),
|
||||||
|
s.GetRequiredService<ServerConfigurationManager>(),
|
||||||
|
s.GetRequiredService<BroadcastService>(),
|
||||||
|
s.GetRequiredService<BroadcastScannerService>(),
|
||||||
|
s.GetRequiredService<DalamudUtilService>()));
|
||||||
collection.AddSingleton(s => new PairManager(s.GetRequiredService<ILogger<PairManager>>(), s.GetRequiredService<PairFactory>(),
|
collection.AddSingleton(s => new PairManager(s.GetRequiredService<ILogger<PairManager>>(), s.GetRequiredService<PairFactory>(),
|
||||||
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), contextMenu, s.GetRequiredService<PairProcessingLimiter>()));
|
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), contextMenu, s.GetRequiredService<PairProcessingLimiter>()));
|
||||||
collection.AddSingleton<RedrawManager>();
|
collection.AddSingleton<RedrawManager>();
|
||||||
@@ -172,9 +183,14 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<IpcCallerPenumbra>(), s.GetRequiredService<IpcCallerGlamourer>(),
|
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<IpcCallerPenumbra>(), s.GetRequiredService<IpcCallerGlamourer>(),
|
||||||
s.GetRequiredService<IpcCallerCustomize>(), s.GetRequiredService<IpcCallerHeels>(), s.GetRequiredService<IpcCallerHonorific>(),
|
s.GetRequiredService<IpcCallerCustomize>(), s.GetRequiredService<IpcCallerHeels>(), s.GetRequiredService<IpcCallerHonorific>(),
|
||||||
s.GetRequiredService<IpcCallerMoodles>(), s.GetRequiredService<IpcCallerPetNames>(), s.GetRequiredService<IpcCallerBrio>()));
|
s.GetRequiredService<IpcCallerMoodles>(), s.GetRequiredService<IpcCallerPetNames>(), s.GetRequiredService<IpcCallerBrio>()));
|
||||||
collection.AddSingleton((s) => new NotificationService(s.GetRequiredService<ILogger<NotificationService>>(),
|
collection.AddSingleton((s) => new NotificationService(
|
||||||
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<DalamudUtilService>(),
|
s.GetRequiredService<ILogger<NotificationService>>(),
|
||||||
notificationManager, chatGui, s.GetRequiredService<LightlessConfigService>()));
|
s.GetRequiredService<LightlessConfigService>(),
|
||||||
|
s.GetRequiredService<DalamudUtilService>(),
|
||||||
|
notificationManager,
|
||||||
|
chatGui,
|
||||||
|
s.GetRequiredService<LightlessMediator>(),
|
||||||
|
s.GetRequiredService<PairRequestService>()));
|
||||||
collection.AddSingleton((s) =>
|
collection.AddSingleton((s) =>
|
||||||
{
|
{
|
||||||
var httpClient = new HttpClient();
|
var httpClient = new HttpClient();
|
||||||
@@ -182,10 +198,12 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("LightlessSync", ver!.Major + "." + ver!.Minor + "." + ver!.Build));
|
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("LightlessSync", ver!.Major + "." + ver!.Minor + "." + ver!.Build));
|
||||||
return httpClient;
|
return httpClient;
|
||||||
});
|
});
|
||||||
|
collection.AddSingleton((s) => new UiThemeConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||||
collection.AddSingleton((s) =>
|
collection.AddSingleton((s) =>
|
||||||
{
|
{
|
||||||
var cfg = new LightlessConfigService(pluginInterface.ConfigDirectory.FullName);
|
var cfg = new LightlessConfigService(pluginInterface.ConfigDirectory.FullName);
|
||||||
LightlessSync.UI.Style.MainStyle.Init(cfg);
|
var theme = s.GetRequiredService<UiThemeConfigService>();
|
||||||
|
LightlessSync.UI.Style.MainStyle.Init(cfg, theme);
|
||||||
return cfg;
|
return cfg;
|
||||||
});
|
});
|
||||||
collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName));
|
collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||||
@@ -197,6 +215,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton((s) => new PlayerPerformanceConfigService(pluginInterface.ConfigDirectory.FullName));
|
collection.AddSingleton((s) => new PlayerPerformanceConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||||
collection.AddSingleton((s) => new CharaDataConfigService(pluginInterface.ConfigDirectory.FullName));
|
collection.AddSingleton((s) => new CharaDataConfigService(pluginInterface.ConfigDirectory.FullName));
|
||||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<LightlessConfigService>());
|
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<LightlessConfigService>());
|
||||||
|
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<UiThemeConfigService>());
|
||||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<ServerConfigService>());
|
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<ServerConfigService>());
|
||||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<NotesConfigService>());
|
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<NotesConfigService>());
|
||||||
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<PairTagConfigService>());
|
collection.AddSingleton<IConfigService<ILightlessConfiguration>>(s => s.GetRequiredService<PairTagConfigService>());
|
||||||
@@ -227,6 +246,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddScoped<WindowMediatorSubscriberBase, CreateSyncshellUI>();
|
collection.AddScoped<WindowMediatorSubscriberBase, CreateSyncshellUI>();
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, EventViewerUI>();
|
collection.AddScoped<WindowMediatorSubscriberBase, EventViewerUI>();
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, CharaDataHubUi>();
|
collection.AddScoped<WindowMediatorSubscriberBase, CharaDataHubUi>();
|
||||||
|
collection.AddScoped<WindowMediatorSubscriberBase, UpdateNotesUi>();
|
||||||
|
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, EditProfileUi>((s) => new EditProfileUi(s.GetRequiredService<ILogger<EditProfileUi>>(),
|
collection.AddScoped<WindowMediatorSubscriberBase, EditProfileUi>((s) => new EditProfileUi(s.GetRequiredService<ILogger<EditProfileUi>>(),
|
||||||
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<FileDialogManager>(),
|
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<FileDialogManager>(),
|
||||||
@@ -235,6 +255,12 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddScoped<WindowMediatorSubscriberBase, BroadcastUI>((s) => new BroadcastUI(s.GetRequiredService<ILogger<BroadcastUI>>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(), s.GetRequiredService<BroadcastService>(), s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<BroadcastScannerService>()));
|
collection.AddScoped<WindowMediatorSubscriberBase, BroadcastUI>((s) => new BroadcastUI(s.GetRequiredService<ILogger<BroadcastUI>>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(), s.GetRequiredService<BroadcastService>(), s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<BroadcastScannerService>()));
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, SyncshellFinderUI>((s) => new SyncshellFinderUI(s.GetRequiredService<ILogger<SyncshellFinderUI>>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(), s.GetRequiredService<BroadcastService>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<BroadcastScannerService>(), s.GetRequiredService<PairManager>(), s.GetRequiredService<DalamudUtilService>()));
|
collection.AddScoped<WindowMediatorSubscriberBase, SyncshellFinderUI>((s) => new SyncshellFinderUI(s.GetRequiredService<ILogger<SyncshellFinderUI>>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(), s.GetRequiredService<BroadcastService>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<BroadcastScannerService>(), s.GetRequiredService<PairManager>(), s.GetRequiredService<DalamudUtilService>()));
|
||||||
collection.AddScoped<IPopupHandler, BanUserPopupHandler>();
|
collection.AddScoped<IPopupHandler, BanUserPopupHandler>();
|
||||||
|
collection.AddScoped<WindowMediatorSubscriberBase, LightlessNotificationUi>((s) =>
|
||||||
|
new LightlessNotificationUi(
|
||||||
|
s.GetRequiredService<ILogger<LightlessNotificationUi>>(),
|
||||||
|
s.GetRequiredService<LightlessMediator>(),
|
||||||
|
s.GetRequiredService<PerformanceCollectorService>(),
|
||||||
|
s.GetRequiredService<LightlessConfigService>()));
|
||||||
collection.AddScoped<IPopupHandler, CensusPopupHandler>();
|
collection.AddScoped<IPopupHandler, CensusPopupHandler>();
|
||||||
collection.AddScoped<CacheCreationService>();
|
collection.AddScoped<CacheCreationService>();
|
||||||
collection.AddScoped<PlayerDataFactory>();
|
collection.AddScoped<PlayerDataFactory>();
|
||||||
@@ -242,7 +268,8 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddScoped((s) => new UiService(s.GetRequiredService<ILogger<UiService>>(), pluginInterface.UiBuilder, s.GetRequiredService<LightlessConfigService>(),
|
collection.AddScoped((s) => new UiService(s.GetRequiredService<ILogger<UiService>>(), pluginInterface.UiBuilder, s.GetRequiredService<LightlessConfigService>(),
|
||||||
s.GetRequiredService<WindowSystem>(), s.GetServices<WindowMediatorSubscriberBase>(),
|
s.GetRequiredService<WindowSystem>(), s.GetServices<WindowMediatorSubscriberBase>(),
|
||||||
s.GetRequiredService<UiFactory>(),
|
s.GetRequiredService<UiFactory>(),
|
||||||
s.GetRequiredService<FileDialogManager>(), s.GetRequiredService<LightlessMediator>()));
|
s.GetRequiredService<FileDialogManager>(),
|
||||||
|
s.GetRequiredService<LightlessMediator>()));
|
||||||
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>()));
|
||||||
@@ -279,4 +306,4 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
_host.StopAsync().GetAwaiter().GetResult();
|
_host.StopAsync().GetAwaiter().GetResult();
|
||||||
_host.Dispose();
|
_host.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -211,6 +211,16 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos
|
|||||||
UpdateSyncshellBroadcasts();
|
UpdateSyncshellBroadcasts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int CountActiveBroadcasts(string? excludeHashedCid = null)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var comparer = StringComparer.Ordinal;
|
||||||
|
return _broadcastCache.Count(entry =>
|
||||||
|
entry.Value.IsBroadcasting &&
|
||||||
|
entry.Value.ExpiryTime > now &&
|
||||||
|
(excludeHashedCid is null || !comparer.Equals(entry.Key, excludeHashedCid)));
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
using LightlessSync.API.Dto.Group;
|
using Dalamud.Interface;
|
||||||
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
|
using LightlessSync.UI;
|
||||||
|
using LightlessSync.UI.Models;
|
||||||
|
using LightlessSync.API.Dto.Group;
|
||||||
using LightlessSync.API.Dto.User;
|
using LightlessSync.API.Dto.User;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
@@ -7,6 +11,7 @@ using LightlessSync.WebAPI;
|
|||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace LightlessSync.Services;
|
namespace LightlessSync.Services;
|
||||||
public class BroadcastService : IHostedService, IMediatorSubscriber
|
public class BroadcastService : IHostedService, IMediatorSubscriber
|
||||||
@@ -16,9 +21,11 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
|||||||
private readonly LightlessMediator _mediator;
|
private readonly LightlessMediator _mediator;
|
||||||
private readonly LightlessConfigService _config;
|
private readonly LightlessConfigService _config;
|
||||||
private readonly DalamudUtilService _dalamudUtil;
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
|
private CancellationTokenSource? _lightfinderCancelTokens;
|
||||||
|
private Action? _connectedHandler;
|
||||||
public LightlessMediator Mediator => _mediator;
|
public LightlessMediator Mediator => _mediator;
|
||||||
|
|
||||||
public bool IsLightFinderAvailable { get; private set; } = true;
|
public bool IsLightFinderAvailable { get; private set; } = false;
|
||||||
|
|
||||||
public bool IsBroadcasting => _config.Current.BroadcastEnabled;
|
public bool IsBroadcasting => _config.Current.BroadcastEnabled;
|
||||||
private bool _syncedOnStartup = false;
|
private bool _syncedOnStartup = false;
|
||||||
@@ -57,24 +64,130 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
|||||||
await action().ConfigureAwait(false);
|
await action().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StartAsync(CancellationToken cancellationToken)
|
private async Task<string?> GetLocalHashedCidAsync(string context)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cid = await _dalamudUtil.GetCIDAsync().ConfigureAwait(false);
|
||||||
|
return cid.ToString().GetHash256();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to resolve CID for {Context}", context);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyBroadcastDisabled(bool forcePublish = false)
|
||||||
|
{
|
||||||
|
bool wasEnabled = _config.Current.BroadcastEnabled;
|
||||||
|
bool hadExpiry = _config.Current.BroadcastTtl != DateTime.MinValue;
|
||||||
|
bool hadRemaining = _remainingTtl.HasValue;
|
||||||
|
|
||||||
|
_config.Current.BroadcastEnabled = false;
|
||||||
|
_config.Current.BroadcastTtl = DateTime.MinValue;
|
||||||
|
|
||||||
|
if (wasEnabled || hadExpiry)
|
||||||
|
_config.Save();
|
||||||
|
|
||||||
|
_remainingTtl = null;
|
||||||
|
_waitingForTtlFetch = false;
|
||||||
|
_syncedOnStartup = false;
|
||||||
|
|
||||||
|
if (forcePublish || wasEnabled || hadRemaining)
|
||||||
|
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryApplyBroadcastEnabled(TimeSpan? ttl, string context)
|
||||||
|
{
|
||||||
|
if (ttl is not { } validTtl || validTtl <= TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Lightfinder enable skipped ({Context}): invalid TTL ({TTL})", context, ttl);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool wasEnabled = _config.Current.BroadcastEnabled;
|
||||||
|
TimeSpan? previousRemaining = _remainingTtl;
|
||||||
|
DateTime previousExpiry = _config.Current.BroadcastTtl;
|
||||||
|
|
||||||
|
var newExpiry = DateTime.UtcNow + validTtl;
|
||||||
|
|
||||||
|
_config.Current.BroadcastEnabled = true;
|
||||||
|
_config.Current.BroadcastTtl = newExpiry;
|
||||||
|
|
||||||
|
if (!wasEnabled || previousExpiry != newExpiry)
|
||||||
|
_config.Save();
|
||||||
|
|
||||||
|
_remainingTtl = validTtl;
|
||||||
|
_waitingForTtlFetch = false;
|
||||||
|
|
||||||
|
if (!wasEnabled || previousRemaining != validTtl)
|
||||||
|
_mediator.Publish(new BroadcastStatusChangedMessage(true, validTtl));
|
||||||
|
|
||||||
|
_logger.LogInformation("Lightfinder broadcast enabled ({Context}), TTL: {TTL}", context, validTtl);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleLightfinderUnavailable(string message, Exception? ex = null)
|
||||||
|
{
|
||||||
|
if (ex != null)
|
||||||
|
_logger.LogWarning(ex, message);
|
||||||
|
else
|
||||||
|
_logger.LogWarning(message);
|
||||||
|
|
||||||
|
IsLightFinderAvailable = false;
|
||||||
|
ApplyBroadcastDisabled(forcePublish: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisconnected()
|
||||||
|
{
|
||||||
|
IsLightFinderAvailable = false;
|
||||||
|
ApplyBroadcastDisabled(forcePublish: true);
|
||||||
|
_logger.LogDebug("Cleared Lightfinder state due to disconnect.");
|
||||||
|
|
||||||
|
_mediator.Publish(new NotificationMessage(
|
||||||
|
"Disconnected from Server",
|
||||||
|
"Your Lightfinder broadcast has been disabled due to disconnection.",
|
||||||
|
NotificationType.Warning));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_mediator.Subscribe<EnableBroadcastMessage>(this, OnEnableBroadcast);
|
_mediator.Subscribe<EnableBroadcastMessage>(this, OnEnableBroadcast);
|
||||||
_mediator.Subscribe<BroadcastStatusChangedMessage>(this, OnBroadcastStatusChanged);
|
_mediator.Subscribe<BroadcastStatusChangedMessage>(this, OnBroadcastStatusChanged);
|
||||||
_mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, OnTick);
|
_mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, OnTick);
|
||||||
|
_mediator.Subscribe<DisconnectedMessage>(this, _ => OnDisconnected());
|
||||||
|
|
||||||
_apiController.OnConnected += () => _ = CheckLightfinderSupportAsync(cancellationToken);
|
IsLightFinderAvailable = false;
|
||||||
//_ = CheckLightfinderSupportAsync(cancellationToken);
|
|
||||||
|
_lightfinderCancelTokens?.Cancel();
|
||||||
|
_lightfinderCancelTokens?.Dispose();
|
||||||
|
_lightfinderCancelTokens = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
|
_connectedHandler = () => _ = CheckLightfinderSupportAsync(_lightfinderCancelTokens.Token);
|
||||||
|
_apiController.OnConnected += _connectedHandler;
|
||||||
|
|
||||||
|
if (_apiController.IsConnected)
|
||||||
|
_ = CheckLightfinderSupportAsync(_lightfinderCancelTokens.Token);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken)
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
_lightfinderCancelTokens?.Cancel();
|
||||||
|
_lightfinderCancelTokens?.Dispose();
|
||||||
|
_lightfinderCancelTokens = null;
|
||||||
|
|
||||||
|
if (_connectedHandler is not null)
|
||||||
|
{
|
||||||
|
_apiController.OnConnected -= _connectedHandler;
|
||||||
|
_connectedHandler = null;
|
||||||
|
}
|
||||||
|
|
||||||
_mediator.UnsubscribeAll(this);
|
_mediator.UnsubscribeAll(this);
|
||||||
_apiController.OnConnected -= () => _ = CheckLightfinderSupportAsync(cancellationToken);
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
// need to rework this, this is cooked
|
|
||||||
private async Task CheckLightfinderSupportAsync(CancellationToken cancellationToken)
|
private async Task CheckLightfinderSupportAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -85,25 +198,59 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
|||||||
if (cancellationToken.IsCancellationRequested)
|
if (cancellationToken.IsCancellationRequested)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var dummy = "0".PadLeft(64, '0');
|
var hashedCid = await GetLocalHashedCidAsync("Lightfinder state check").ConfigureAwait(false);
|
||||||
|
if (string.IsNullOrEmpty(hashedCid))
|
||||||
|
return;
|
||||||
|
|
||||||
await _apiController.IsUserBroadcasting(dummy).ConfigureAwait(false);
|
BroadcastStatusInfoDto? status = null;
|
||||||
await _apiController.SetBroadcastStatus(dummy, true, null).ConfigureAwait(false);
|
try
|
||||||
await _apiController.GetBroadcastTtl(dummy).ConfigureAwait(false);
|
{
|
||||||
await _apiController.AreUsersBroadcasting([dummy]).ConfigureAwait(false);
|
status = await _apiController.IsUserBroadcasting(hashedCid).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (HubException ex) when (ex.Message.Contains("Method does not exist", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
HandleLightfinderUnavailable("Lightfinder unavailable on server (required method missing).", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsLightFinderAvailable)
|
||||||
|
_logger.LogInformation("Lightfinder is available.");
|
||||||
|
|
||||||
IsLightFinderAvailable = true;
|
IsLightFinderAvailable = true;
|
||||||
_logger.LogInformation("Lightfinder is available.");
|
|
||||||
}
|
|
||||||
catch (HubException ex) when (ex.Message.Contains("Method does not exist"))
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Lightfinder unavailable: required method missing.");
|
|
||||||
IsLightFinderAvailable = false;
|
|
||||||
|
|
||||||
_config.Current.BroadcastEnabled = false;
|
bool isBroadcasting = status?.IsBroadcasting == true;
|
||||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
TimeSpan? ttl = status?.TTL;
|
||||||
_config.Save();
|
|
||||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
if (isBroadcasting)
|
||||||
|
{
|
||||||
|
if (ttl is not { } remaining || remaining <= TimeSpan.Zero)
|
||||||
|
ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (TryApplyBroadcastEnabled(ttl, "server handshake"))
|
||||||
|
{
|
||||||
|
_syncedOnStartup = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
isBroadcasting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isBroadcasting)
|
||||||
|
{
|
||||||
|
ApplyBroadcastDisabled(forcePublish: true);
|
||||||
|
_logger.LogInformation("Lightfinder is available but no active broadcast was found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_config.Current.LightfinderAutoEnableOnConnect && !isBroadcasting)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Auto-enabling Lightfinder broadcast after reconnect.");
|
||||||
|
_mediator.Publish(new EnableBroadcastMessage(hashedCid, true));
|
||||||
|
|
||||||
|
_mediator.Publish(new NotificationMessage(
|
||||||
|
"Broadcast Auto-Enabled",
|
||||||
|
"Your Lightfinder broadcast has been automatically enabled.",
|
||||||
|
NotificationType.Info));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -111,14 +258,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Lightfinder check failed.");
|
HandleLightfinderUnavailable("Lightfinder check failed.", ex);
|
||||||
IsLightFinderAvailable = false;
|
|
||||||
|
|
||||||
_config.Current.BroadcastEnabled = false;
|
|
||||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
|
||||||
_config.Save();
|
|
||||||
|
|
||||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,46 +279,38 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await _apiController.SetBroadcastStatus(msg.HashedCid, msg.Enabled, groupDto).ConfigureAwait(false);
|
await _apiController.SetBroadcastStatus(msg.Enabled, groupDto).ConfigureAwait(false);
|
||||||
|
|
||||||
_logger.LogDebug("Broadcast {Status} for {Cid}", msg.Enabled ? "enabled" : "disabled", msg.HashedCid);
|
_logger.LogDebug("Broadcast {Status} for {Cid}", msg.Enabled ? "enabled" : "disabled", msg.HashedCid);
|
||||||
|
|
||||||
if (!msg.Enabled)
|
if (!msg.Enabled)
|
||||||
{
|
{
|
||||||
_config.Current.BroadcastEnabled = false;
|
ApplyBroadcastDisabled(forcePublish: true);
|
||||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
|
||||||
_config.Save();
|
|
||||||
|
|
||||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
|
||||||
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Disabled Lightfinder for Player: {msg.HashedCid}")));
|
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Disabled Lightfinder for Player: {msg.HashedCid}")));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_waitingForTtlFetch = true;
|
_waitingForTtlFetch = true;
|
||||||
|
|
||||||
TimeSpan? ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false);
|
try
|
||||||
|
|
||||||
if (ttl is { } remaining && remaining > TimeSpan.Zero)
|
|
||||||
{
|
{
|
||||||
_config.Current.BroadcastTtl = DateTime.UtcNow + remaining;
|
TimeSpan? ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false);
|
||||||
_config.Current.BroadcastEnabled = true;
|
|
||||||
_config.Save();
|
|
||||||
|
|
||||||
_logger.LogDebug("Fetched TTL from server: {TTL}", remaining);
|
if (TryApplyBroadcastEnabled(ttl, "client request"))
|
||||||
_mediator.Publish(new BroadcastStatusChangedMessage(true, remaining));
|
{
|
||||||
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Enabled Lightfinder for Player: {msg.HashedCid}")));
|
_logger.LogDebug("Fetched TTL from server: {TTL}", ttl);
|
||||||
|
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Enabled Lightfinder for Player: {msg.HashedCid}")));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ApplyBroadcastDisabled(forcePublish: true);
|
||||||
|
_logger.LogWarning("No valid TTL returned after enabling broadcast. Disabling.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
finally
|
||||||
{
|
{
|
||||||
_logger.LogWarning("No valid TTL returned after enabling broadcast. Disabling.");
|
_waitingForTtlFetch = false;
|
||||||
_config.Current.BroadcastEnabled = false;
|
|
||||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
|
||||||
_config.Save();
|
|
||||||
|
|
||||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_waitingForTtlFetch = false;
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -219,17 +351,24 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TimeSpan?> GetBroadcastTtlAsync(string cid)
|
public async Task<TimeSpan?> GetBroadcastTtlAsync(string? cidForLog = null)
|
||||||
{
|
{
|
||||||
TimeSpan? ttl = null;
|
TimeSpan? ttl = null;
|
||||||
await RequireConnectionAsync(nameof(GetBroadcastTtlAsync), async () => {
|
await RequireConnectionAsync(nameof(GetBroadcastTtlAsync), async () => {
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ttl = await _apiController.GetBroadcastTtl(cid).ConfigureAwait(false);
|
ttl = await _apiController.GetBroadcastTtl().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Failed to fetch broadcast TTL for {cid}", cid);
|
if (cidForLog is { Length: > 0 })
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to fetch broadcast TTL for {Cid}", cidForLog);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to fetch broadcast TTL");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
return ttl;
|
return ttl;
|
||||||
@@ -266,9 +405,14 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
public async void ToggleBroadcast()
|
public async void ToggleBroadcast()
|
||||||
{
|
{
|
||||||
|
|
||||||
if (!IsLightFinderAvailable)
|
if (!IsLightFinderAvailable)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("ToggleBroadcast - Lightfinder is not available.");
|
_logger.LogWarning("ToggleBroadcast - Lightfinder is not available.");
|
||||||
|
_mediator.Publish(new NotificationMessage(
|
||||||
|
"Broadcast Unavailable",
|
||||||
|
"Lightfinder is not available on this server.",
|
||||||
|
NotificationType.Error));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,10 +422,19 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
|||||||
if (!_config.Current.BroadcastEnabled && cooldown is { } cd && cd > TimeSpan.Zero)
|
if (!_config.Current.BroadcastEnabled && cooldown is { } cd && cd > TimeSpan.Zero)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Cooldown active. Must wait {Remaining}s before re-enabling.", cd.TotalSeconds);
|
_logger.LogWarning("Cooldown active. Must wait {Remaining}s before re-enabling.", cd.TotalSeconds);
|
||||||
|
_mediator.Publish(new NotificationMessage(
|
||||||
|
"Broadcast Cooldown",
|
||||||
|
$"Please wait {cd.TotalSeconds:F0} seconds before re-enabling broadcast.",
|
||||||
|
NotificationType.Warning));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
|
var hashedCid = await GetLocalHashedCidAsync(nameof(ToggleBroadcast)).ConfigureAwait(false);
|
||||||
|
if (string.IsNullOrEmpty(hashedCid))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("ToggleBroadcast - unable to resolve CID.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -297,10 +450,19 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
|||||||
_logger.LogDebug("Toggling broadcast. Server currently broadcasting: {ServerStatus}, setting to: {NewStatus}", isCurrentlyBroadcasting, newStatus);
|
_logger.LogDebug("Toggling broadcast. Server currently broadcasting: {ServerStatus}, setting to: {NewStatus}", isCurrentlyBroadcasting, newStatus);
|
||||||
|
|
||||||
_mediator.Publish(new EnableBroadcastMessage(hashedCid, newStatus));
|
_mediator.Publish(new EnableBroadcastMessage(hashedCid, newStatus));
|
||||||
|
|
||||||
|
_mediator.Publish(new NotificationMessage(
|
||||||
|
newStatus ? "Broadcast Enabled" : "Broadcast Disabled",
|
||||||
|
newStatus ? "Your Lightfinder broadcast has been enabled." : "Your Lightfinder broadcast has been disabled.",
|
||||||
|
NotificationType.Info));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Failed to determine current broadcast status for toggle");
|
_logger.LogError(ex, "Failed to determine current broadcast status for toggle");
|
||||||
|
_mediator.Publish(new NotificationMessage(
|
||||||
|
"Broadcast Toggle Failed",
|
||||||
|
$"Failed to toggle broadcast: {ex.Message}",
|
||||||
|
NotificationType.Error));
|
||||||
}
|
}
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -321,31 +483,31 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
|||||||
await RequireConnectionAsync(nameof(OnTick), async () => {
|
await RequireConnectionAsync(nameof(OnTick), async () => {
|
||||||
if (!_syncedOnStartup && _config.Current.BroadcastEnabled)
|
if (!_syncedOnStartup && _config.Current.BroadcastEnabled)
|
||||||
{
|
{
|
||||||
_syncedOnStartup = true;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
|
var hashedCid = await GetLocalHashedCidAsync("startup TTL refresh").ConfigureAwait(false);
|
||||||
TimeSpan? ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false);
|
if (string.IsNullOrEmpty(hashedCid))
|
||||||
if (ttl is { }
|
|
||||||
remaining && remaining > TimeSpan.Zero)
|
|
||||||
{
|
{
|
||||||
_config.Current.BroadcastTtl = DateTime.UtcNow + remaining;
|
_logger.LogDebug("Skipping TTL refresh; hashed CID unavailable.");
|
||||||
_config.Current.BroadcastEnabled = true;
|
return;
|
||||||
_config.Save();
|
}
|
||||||
_logger.LogDebug("Refreshed broadcast TTL from server on first OnTick: {TTL}", remaining);
|
|
||||||
|
TimeSpan? ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false);
|
||||||
|
if (TryApplyBroadcastEnabled(ttl, "startup TTL refresh"))
|
||||||
|
{
|
||||||
|
_syncedOnStartup = true;
|
||||||
|
_logger.LogDebug("Refreshed broadcast TTL from server on first OnTick: {TTL}", ttl);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.LogWarning("No valid TTL found on OnTick. Disabling broadcast state.");
|
_logger.LogWarning("No valid TTL found on OnTick. Disabling broadcast state.");
|
||||||
_config.Current.BroadcastEnabled = false;
|
ApplyBroadcastDisabled(forcePublish: true);
|
||||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
|
||||||
_config.Save();
|
|
||||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Failed to refresh TTL in OnTick");
|
_logger.LogError(ex, "Failed to refresh TTL in OnTick");
|
||||||
|
_syncedOnStartup = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (_config.Current.BroadcastEnabled)
|
if (_config.Current.BroadcastEnabled)
|
||||||
@@ -362,10 +524,8 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
|||||||
if (_remainingTtl == null)
|
if (_remainingTtl == null)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Broadcast TTL expired. Disabling broadcast locally.");
|
_logger.LogDebug("Broadcast TTL expired. Disabling broadcast locally.");
|
||||||
_config.Current.BroadcastEnabled = false;
|
ApplyBroadcastDisabled(forcePublish: true);
|
||||||
_config.Current.BroadcastTtl = DateTime.MinValue;
|
ShowBroadcastExpiredNotification();
|
||||||
_config.Save();
|
|
||||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -374,4 +534,49 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
|||||||
}
|
}
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ShowBroadcastExpiredNotification()
|
||||||
|
{
|
||||||
|
var notification = new LightlessNotification
|
||||||
|
{
|
||||||
|
Id = "broadcast_expired",
|
||||||
|
Title = "Broadcast Expired",
|
||||||
|
Message = "Your Lightfinder broadcast has expired after 3 hours. Would you like to re-enable it?",
|
||||||
|
Type = NotificationType.PairRequest,
|
||||||
|
Duration = TimeSpan.FromSeconds(180),
|
||||||
|
Actions = new List<LightlessNotificationAction>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = "re_enable",
|
||||||
|
Label = "Re-enable",
|
||||||
|
Icon = FontAwesomeIcon.Plus,
|
||||||
|
Color = UIColors.Get("PairBlue"),
|
||||||
|
IsPrimary = true,
|
||||||
|
OnClick = (n) =>
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Re-enabling broadcast from notification");
|
||||||
|
ToggleBroadcast();
|
||||||
|
n.IsDismissed = true;
|
||||||
|
n.IsAnimatingOut = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = "close",
|
||||||
|
Label = "Close",
|
||||||
|
Icon = FontAwesomeIcon.Times,
|
||||||
|
Color = UIColors.Get("DimRed"),
|
||||||
|
OnClick = (n) =>
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Broadcast expiration notification dismissed");
|
||||||
|
n.IsDismissed = true;
|
||||||
|
n.IsAnimatingOut = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_mediator.Publish(new LightlessNotificationMessage(notification));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ public sealed class CommandManagerService : IDisposable
|
|||||||
"\t /light gpose - Opens the Lightless Character Data Hub window" + Environment.NewLine +
|
"\t /light gpose - Opens the Lightless Character Data Hub window" + Environment.NewLine +
|
||||||
"\t /light analyze - Opens the Lightless Character Data Analysis window" + Environment.NewLine +
|
"\t /light analyze - Opens the Lightless Character Data Analysis window" + Environment.NewLine +
|
||||||
"\t /light settings - Opens the Lightless Settings window" + Environment.NewLine +
|
"\t /light settings - Opens the Lightless Settings window" + Environment.NewLine +
|
||||||
"\t /light lightfinder - Opens the Lightfinder window"
|
"\t /light finder - Opens the Lightfinder window"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ public sealed class CommandManagerService : IDisposable
|
|||||||
{
|
{
|
||||||
_mediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
|
_mediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
|
||||||
}
|
}
|
||||||
else if (string.Equals(splitArgs[0], "lightfinder", StringComparison.OrdinalIgnoreCase))
|
else if (string.Equals(splitArgs[0], "finder", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
_mediator.Publish(new UiToggleMessage(typeof(BroadcastUI)));
|
_mediator.Publish(new UiToggleMessage(typeof(BroadcastUI)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ internal class ContextMenuService : IHostedService
|
|||||||
var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address);
|
var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address);
|
||||||
|
|
||||||
_logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid);
|
_logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid);
|
||||||
await _apiController.TryPairWithContentId(receiverCid, senderCid).ConfigureAwait(false);
|
await _apiController.TryPairWithContentId(receiverCid).ConfigureAwait(false);
|
||||||
if (!string.IsNullOrWhiteSpace(receiverCid))
|
if (!string.IsNullOrWhiteSpace(receiverCid))
|
||||||
{
|
{
|
||||||
_pairRequestService.RemoveRequest(receiverCid);
|
_pairRequestService.RemoveRequest(receiverCid);
|
||||||
|
|||||||
6
LightlessSync/Services/LightlessGroupProfileData.cs
Normal file
6
LightlessSync/Services/LightlessGroupProfileData.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace LightlessSync.Services;
|
||||||
|
|
||||||
|
public record LightlessGroupProfileData(string Base64ProfilePicture, string Description, int[] Tags, bool IsNsfw, bool IsDisabled)
|
||||||
|
{
|
||||||
|
public Lazy<byte[]> ImageData { get; } = new Lazy<byte[]>(Convert.FromBase64String(Base64ProfilePicture));
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
|||||||
namespace LightlessSync.Services;
|
namespace LightlessSync.Services;
|
||||||
|
|
||||||
public record LightlessProfileData(bool IsFlagged, bool IsNSFW, string Base64ProfilePicture, string Base64SupporterPicture, string Description)
|
public record LightlessUserProfileData(bool IsFlagged, bool IsNSFW, string Base64ProfilePicture, string Base64SupporterPicture, string Description)
|
||||||
{
|
{
|
||||||
public Lazy<byte[]> ImageData { get; } = new Lazy<byte[]>(Convert.FromBase64String(Base64ProfilePicture));
|
public Lazy<byte[]> ImageData { get; } = new Lazy<byte[]>(Convert.FromBase64String(Base64ProfilePicture));
|
||||||
public Lazy<byte[]> SupporterImageData { get; } = new Lazy<byte[]>(string.IsNullOrEmpty(Base64SupporterPicture) ? [] : Convert.FromBase64String(Base64SupporterPicture));
|
public Lazy<byte[]> SupporterImageData { get; } = new Lazy<byte[]>(string.IsNullOrEmpty(Base64SupporterPicture) ? [] : Convert.FromBase64String(Base64SupporterPicture));
|
||||||
@@ -17,6 +17,7 @@ namespace LightlessSync.Services.Mediator;
|
|||||||
public record SwitchToIntroUiMessage : MessageBase;
|
public record SwitchToIntroUiMessage : MessageBase;
|
||||||
public record SwitchToMainUiMessage : MessageBase;
|
public record SwitchToMainUiMessage : MessageBase;
|
||||||
public record OpenSettingsUiMessage : MessageBase;
|
public record OpenSettingsUiMessage : MessageBase;
|
||||||
|
public record OpenLightfinderSettingsMessage : MessageBase;
|
||||||
public record DalamudLoginMessage : MessageBase;
|
public record DalamudLoginMessage : MessageBase;
|
||||||
public record DalamudLogoutMessage : MessageBase;
|
public record DalamudLogoutMessage : MessageBase;
|
||||||
public record PriorityFrameworkUpdateMessage : SameThreadMessage;
|
public record PriorityFrameworkUpdateMessage : SameThreadMessage;
|
||||||
@@ -47,24 +48,30 @@ public record PetNamesMessage(string PetNicknamesData) : MessageBase;
|
|||||||
public record HonorificReadyMessage : MessageBase;
|
public record HonorificReadyMessage : MessageBase;
|
||||||
public record TransientResourceChangedMessage(IntPtr Address) : MessageBase;
|
public record TransientResourceChangedMessage(IntPtr Address) : MessageBase;
|
||||||
public record HaltScanMessage(string Source) : MessageBase;
|
public record HaltScanMessage(string Source) : MessageBase;
|
||||||
public record ResumeScanMessage(string Source) : MessageBase;
|
|
||||||
public record NotificationMessage
|
public record NotificationMessage
|
||||||
(string Title, string Message, NotificationType Type, TimeSpan? TimeShownOnScreen = null) : MessageBase;
|
(string Title, string Message, NotificationType Type, TimeSpan? TimeShownOnScreen = null) : MessageBase;
|
||||||
|
public record PerformanceNotificationMessage
|
||||||
|
(string Title, string Message, UserData UserData, bool IsPaused, string PlayerName) : MessageBase;
|
||||||
public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage;
|
public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage;
|
||||||
public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage;
|
public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : SameThreadMessage;
|
||||||
public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage;
|
public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage;
|
||||||
|
public record LightlessNotificationMessage(LightlessSync.UI.Models.LightlessNotification Notification) : MessageBase;
|
||||||
|
public record LightlessNotificationDismissMessage(string NotificationId) : MessageBase;
|
||||||
|
public record ClearAllNotificationsMessage : MessageBase;
|
||||||
public record CharacterDataAnalyzedMessage : MessageBase;
|
public record CharacterDataAnalyzedMessage : MessageBase;
|
||||||
public record PenumbraStartRedrawMessage(IntPtr Address) : MessageBase;
|
public record PenumbraStartRedrawMessage(IntPtr Address) : MessageBase;
|
||||||
public record PenumbraEndRedrawMessage(IntPtr Address) : MessageBase;
|
public record PenumbraEndRedrawMessage(IntPtr Address) : MessageBase;
|
||||||
public record HubReconnectingMessage(Exception? Exception) : SameThreadMessage;
|
public record HubReconnectingMessage(Exception? Exception) : SameThreadMessage;
|
||||||
public record HubReconnectedMessage(string? Arg) : SameThreadMessage;
|
public record HubReconnectedMessage(string? Arg) : SameThreadMessage;
|
||||||
public record HubClosedMessage(Exception? Exception) : SameThreadMessage;
|
public record HubClosedMessage(Exception? Exception) : SameThreadMessage;
|
||||||
|
public record ResumeScanMessage(string Source) : MessageBase;
|
||||||
public record DownloadReadyMessage(Guid RequestId) : MessageBase;
|
public record DownloadReadyMessage(Guid RequestId) : MessageBase;
|
||||||
public record DownloadStartedMessage(GameObjectHandler DownloadId, Dictionary<string, FileDownloadStatus> DownloadStatus) : MessageBase;
|
public record DownloadStartedMessage(GameObjectHandler DownloadId, Dictionary<string, FileDownloadStatus> DownloadStatus) : MessageBase;
|
||||||
public record DownloadFinishedMessage(GameObjectHandler DownloadId) : MessageBase;
|
public record DownloadFinishedMessage(GameObjectHandler DownloadId) : MessageBase;
|
||||||
public record UiToggleMessage(Type UiType) : MessageBase;
|
public record UiToggleMessage(Type UiType) : MessageBase;
|
||||||
public record PlayerUploadingMessage(GameObjectHandler Handler, bool IsUploading) : MessageBase;
|
public record PlayerUploadingMessage(GameObjectHandler Handler, bool IsUploading) : MessageBase;
|
||||||
public record ClearProfileDataMessage(UserData? UserData = null) : MessageBase;
|
public record ClearProfileUserDataMessage(UserData? UserData = null) : MessageBase;
|
||||||
|
public record ClearProfileGroupDataMessage(GroupData? GroupData = null) : MessageBase;
|
||||||
public record CyclePauseMessage(UserData UserData) : MessageBase;
|
public record CyclePauseMessage(UserData UserData) : MessageBase;
|
||||||
public record PauseMessage(UserData UserData) : MessageBase;
|
public record PauseMessage(UserData UserData) : MessageBase;
|
||||||
public record ProfilePopoutToggle(Pair? Pair) : MessageBase;
|
public record ProfilePopoutToggle(Pair? Pair) : MessageBase;
|
||||||
@@ -101,7 +108,9 @@ public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase
|
|||||||
public record EnableBroadcastMessage(string HashedCid, bool Enabled) : MessageBase;
|
public record EnableBroadcastMessage(string HashedCid, bool Enabled) : MessageBase;
|
||||||
public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl) : MessageBase;
|
public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl) : MessageBase;
|
||||||
public record SyncshellBroadcastsUpdatedMessage : MessageBase;
|
public record SyncshellBroadcastsUpdatedMessage : MessageBase;
|
||||||
|
public record PairRequestReceivedMessage(string HashedCid, string Message) : MessageBase;
|
||||||
public record PairRequestsUpdatedMessage : MessageBase;
|
public record PairRequestsUpdatedMessage : MessageBase;
|
||||||
|
public record PairDownloadStatusMessage(List<(string PlayerName, float Progress, string Status)> DownloadStatus, int QueueWaiting) : MessageBase;
|
||||||
public record VisibilityChange : MessageBase;
|
public record VisibilityChange : MessageBase;
|
||||||
#pragma warning restore S2094
|
#pragma warning restore S2094
|
||||||
#pragma warning restore MA0048 // File name must match type name
|
#pragma warning restore MA0048 // File name must match type name
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using Dalamud.Interface.Windowing;
|
using Dalamud.Interface.Windowing;
|
||||||
using LightlessSync.UI.Style;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace LightlessSync.Services.Mediator;
|
namespace LightlessSync.Services.Mediator;
|
||||||
@@ -34,18 +33,6 @@ public abstract class WindowMediatorSubscriberBase : Window, IMediatorSubscriber
|
|||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void PreDraw()
|
|
||||||
{
|
|
||||||
base.PreDraw();
|
|
||||||
MainStyle.PushStyle(); // internally checks ShouldUseTheme
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void PostDraw()
|
|
||||||
{
|
|
||||||
MainStyle.PopStyle(); // always attempts to pop if pushed
|
|
||||||
base.PostDraw();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Draw()
|
public override void Draw()
|
||||||
{
|
{
|
||||||
_performanceCollectorService.LogPerformance(this, $"Draw", DrawInternal);
|
_performanceCollectorService.LogPerformance(this, $"Draw", DrawInternal);
|
||||||
|
|||||||
@@ -208,7 +208,13 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
|||||||
|
|
||||||
for (int i = 0; i < ui3DModule->NamePlateObjectInfoCount; ++i)
|
for (int i = 0; i < ui3DModule->NamePlateObjectInfoCount; ++i)
|
||||||
{
|
{
|
||||||
var objectInfo = ui3DModule->NamePlateObjectInfoPointers[i].Value;
|
if (ui3DModule->NamePlateObjectInfoPointers.IsEmpty) continue;
|
||||||
|
|
||||||
|
var objectInfoPtr = ui3DModule->NamePlateObjectInfoPointers[i];
|
||||||
|
|
||||||
|
if (objectInfoPtr == null) continue;
|
||||||
|
|
||||||
|
var objectInfo = objectInfoPtr.Value;
|
||||||
|
|
||||||
if (objectInfo == null || objectInfo->GameObject == null)
|
if (objectInfo == null || objectInfo->GameObject == null)
|
||||||
continue;
|
continue;
|
||||||
@@ -221,6 +227,9 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
|||||||
if (pNode == null)
|
if (pNode == null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (mpNameplateAddon == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)objectInfo->GameObject);
|
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)objectInfo->GameObject);
|
||||||
|
|
||||||
if (cid == null || !_activeBroadcastingCids.Contains(cid))
|
if (cid == null || !_activeBroadcastingCids.Contains(cid))
|
||||||
@@ -247,9 +256,17 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
|||||||
var pNameplateIconNode = nameplateObject.MarkerIcon;
|
var pNameplateIconNode = nameplateObject.MarkerIcon;
|
||||||
var pNameplateResNode = nameplateObject.NameContainer;
|
var pNameplateResNode = nameplateObject.NameContainer;
|
||||||
var pNameplateTextNode = nameplateObject.NameText;
|
var pNameplateTextNode = nameplateObject.NameText;
|
||||||
bool IsVisible = pNameplateIconNode->AtkResNode.IsVisible() || (pNameplateResNode->IsVisible() && pNameplateTextNode->AtkResNode.IsVisible());
|
bool IsVisible = pNameplateIconNode->AtkResNode.IsVisible() || (pNameplateResNode->IsVisible() && pNameplateTextNode->AtkResNode.IsVisible()) || _configService.Current.LightfinderLabelShowHidden;
|
||||||
pNode->AtkResNode.ToggleVisibility(IsVisible);
|
pNode->AtkResNode.ToggleVisibility(IsVisible);
|
||||||
|
|
||||||
|
if (nameplateObject.RootComponentNode == null ||
|
||||||
|
nameplateObject.NameContainer == null ||
|
||||||
|
nameplateObject.NameText == null)
|
||||||
|
{
|
||||||
|
pNode->AtkResNode.ToggleVisibility(false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var nameContainer = nameplateObject.NameContainer;
|
var nameContainer = nameplateObject.NameContainer;
|
||||||
var nameText = nameplateObject.NameText;
|
var nameText = nameplateObject.NameText;
|
||||||
|
|
||||||
@@ -259,8 +276,8 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var labelColor = UIColors.Get("LightlessPurple");
|
var labelColor = UIColors.Get("Lightfinder");
|
||||||
var edgeColor = UIColors.Get("FullBlack");
|
var edgeColor = UIColors.Get("LightfinderEdge");
|
||||||
var config = _configService.Current;
|
var config = _configService.Current;
|
||||||
|
|
||||||
var scaleMultiplier = System.Math.Clamp(config.LightfinderLabelScale, 0.5f, 2.0f);
|
var scaleMultiplier = System.Math.Clamp(config.LightfinderLabelScale, 0.5f, 2.0f);
|
||||||
@@ -360,33 +377,35 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
|||||||
}
|
}
|
||||||
int positionX;
|
int positionX;
|
||||||
|
|
||||||
|
|
||||||
|
if (!config.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal)))
|
||||||
|
labelContent = DefaultLabelText;
|
||||||
|
|
||||||
|
pNode->FontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed;
|
||||||
|
|
||||||
|
pNode->SetText(labelContent);
|
||||||
|
|
||||||
|
if (!config.LightfinderLabelUseIcon)
|
||||||
|
{
|
||||||
|
pNode->TextFlags &= ~TextFlags.AutoAdjustNodeSize;
|
||||||
|
pNode->AtkResNode.Width = 0;
|
||||||
|
nodeWidth = (int)pNode->AtkResNode.GetWidth();
|
||||||
|
if (nodeWidth <= 0)
|
||||||
|
nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
||||||
|
pNode->AtkResNode.Width = (ushort)nodeWidth;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pNode->TextFlags |= TextFlags.AutoAdjustNodeSize;
|
||||||
|
pNode->AtkResNode.Width = 0;
|
||||||
|
nodeWidth = pNode->AtkResNode.GetWidth();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (config.LightfinderAutoAlign && nameContainer != null && hasValidOffset)
|
if (config.LightfinderAutoAlign && nameContainer != null && hasValidOffset)
|
||||||
{
|
{
|
||||||
var nameplateWidth = (int)nameContainer->Width;
|
var nameplateWidth = (int)nameContainer->Width;
|
||||||
|
|
||||||
if (!config.LightfinderLabelUseIcon)
|
|
||||||
{
|
|
||||||
pNode->TextFlags &= ~TextFlags.AutoAdjustNodeSize;
|
|
||||||
pNode->AtkResNode.Width = 0;
|
|
||||||
pNode->SetText(labelContent);
|
|
||||||
|
|
||||||
nodeWidth = (int)pNode->AtkResNode.GetWidth();
|
|
||||||
if (nodeWidth <= 0)
|
|
||||||
nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
|
||||||
|
|
||||||
if (nodeWidth > nameplateWidth)
|
|
||||||
nodeWidth = nameplateWidth;
|
|
||||||
|
|
||||||
pNode->AtkResNode.Width = (ushort)nodeWidth;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
pNode->TextFlags |= TextFlags.AutoAdjustNodeSize;
|
|
||||||
pNode->AtkResNode.Width = 0;
|
|
||||||
pNode->SetText(labelContent);
|
|
||||||
nodeWidth = (int)pNode->AtkResNode.GetWidth();
|
|
||||||
}
|
|
||||||
|
|
||||||
int leftPos = nameplateWidth / 8;
|
int leftPos = nameplateWidth / 8;
|
||||||
int rightPos = nameplateWidth - nodeWidth - (nameplateWidth / 8);
|
int rightPos = nameplateWidth - nodeWidth - (nameplateWidth / 8);
|
||||||
int centrePos = (nameplateWidth - nodeWidth) / 2;
|
int centrePos = (nameplateWidth - nodeWidth) / 2;
|
||||||
@@ -414,7 +433,7 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
|||||||
positionX = 58 + config.LightfinderLabelOffsetX;
|
positionX = 58 + config.LightfinderLabelOffsetX;
|
||||||
alignment = AlignmentType.Bottom;
|
alignment = AlignmentType.Bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
positionY += config.LightfinderLabelOffsetY;
|
positionY += config.LightfinderLabelOffsetY;
|
||||||
|
|
||||||
alignment = (AlignmentType)System.Math.Clamp((int)alignment, 0, 8);
|
alignment = (AlignmentType)System.Math.Clamp((int)alignment, 0, 8);
|
||||||
|
|||||||
@@ -1,102 +1,438 @@
|
|||||||
using Dalamud.Game.Text.SeStringHandling;
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.ImGuiNotification;
|
using Dalamud.Interface.ImGuiNotification;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.LightlessConfiguration.Models;
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
|
using LightlessSync.UI;
|
||||||
|
using LightlessSync.UI.Models;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
|
using LightlessSync.API.Data;
|
||||||
using NotificationType = LightlessSync.LightlessConfiguration.Models.NotificationType;
|
using NotificationType = LightlessSync.LightlessConfiguration.Models.NotificationType;
|
||||||
|
|
||||||
namespace LightlessSync.Services;
|
namespace LightlessSync.Services;
|
||||||
|
|
||||||
public class NotificationService : DisposableMediatorSubscriberBase, IHostedService
|
public class NotificationService : DisposableMediatorSubscriberBase, IHostedService
|
||||||
{
|
{
|
||||||
|
private readonly ILogger<NotificationService> _logger;
|
||||||
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly DalamudUtilService _dalamudUtilService;
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
private readonly INotificationManager _notificationManager;
|
private readonly INotificationManager _notificationManager;
|
||||||
private readonly IChatGui _chatGui;
|
private readonly IChatGui _chatGui;
|
||||||
private readonly LightlessConfigService _configurationService;
|
private readonly PairRequestService _pairRequestService;
|
||||||
|
private readonly HashSet<string> _shownPairRequestNotifications = new();
|
||||||
|
|
||||||
public NotificationService(ILogger<NotificationService> logger, LightlessMediator mediator,
|
public NotificationService(
|
||||||
|
ILogger<NotificationService> logger,
|
||||||
|
LightlessConfigService configService,
|
||||||
DalamudUtilService dalamudUtilService,
|
DalamudUtilService dalamudUtilService,
|
||||||
INotificationManager notificationManager,
|
INotificationManager notificationManager,
|
||||||
IChatGui chatGui, LightlessConfigService configurationService) : base(logger, mediator)
|
IChatGui chatGui,
|
||||||
|
LightlessMediator mediator,
|
||||||
|
PairRequestService pairRequestService) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_configService = configService;
|
||||||
_dalamudUtilService = dalamudUtilService;
|
_dalamudUtilService = dalamudUtilService;
|
||||||
_notificationManager = notificationManager;
|
_notificationManager = notificationManager;
|
||||||
_chatGui = chatGui;
|
_chatGui = chatGui;
|
||||||
_configurationService = configurationService;
|
_pairRequestService = pairRequestService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Mediator.Subscribe<NotificationMessage>(this, ShowNotification);
|
Mediator.Subscribe<NotificationMessage>(this, HandleNotificationMessage);
|
||||||
|
Mediator.Subscribe<PairRequestReceivedMessage>(this, HandlePairRequestReceived);
|
||||||
|
Mediator.Subscribe<PairRequestsUpdatedMessage>(this, HandlePairRequestsUpdated);
|
||||||
|
Mediator.Subscribe<PairDownloadStatusMessage>(this, HandlePairDownloadStatus);
|
||||||
|
Mediator.Subscribe<PerformanceNotificationMessage>(this, HandlePerformanceNotification);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken)
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PrintErrorChat(string? message)
|
public void ShowNotification(string title, string message, NotificationType type = NotificationType.Info,
|
||||||
|
TimeSpan? duration = null, List<LightlessNotificationAction>? actions = null, uint? soundEffectId = null)
|
||||||
{
|
{
|
||||||
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Error: " + message);
|
var notification = CreateNotification(title, message, type, duration, actions, soundEffectId);
|
||||||
_chatGui.PrintError(se.BuiltString);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PrintInfoChat(string? message)
|
if (_configService.Current.AutoDismissOnAction && notification.Actions.Any())
|
||||||
{
|
|
||||||
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Info: ").AddItalics(message ?? string.Empty);
|
|
||||||
_chatGui.Print(se.BuiltString);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PrintWarnChat(string? message)
|
|
||||||
{
|
|
||||||
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ").AddUiForeground("Warning: " + (message ?? string.Empty), 31).AddUiForegroundOff();
|
|
||||||
_chatGui.Print(se.BuiltString);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ShowChat(NotificationMessage msg)
|
|
||||||
{
|
|
||||||
switch (msg.Type)
|
|
||||||
{
|
{
|
||||||
case NotificationType.Info:
|
WrapActionsWithAutoDismiss(notification);
|
||||||
PrintInfoChat(msg.Message);
|
}
|
||||||
break;
|
|
||||||
|
|
||||||
case NotificationType.Warning:
|
if (notification.SoundEffectId.HasValue)
|
||||||
PrintWarnChat(msg.Message);
|
{
|
||||||
break;
|
PlayNotificationSound(notification.SoundEffectId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
case NotificationType.Error:
|
Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||||
PrintErrorChat(msg.Message);
|
}
|
||||||
break;
|
|
||||||
|
private LightlessNotification CreateNotification(string title, string message, NotificationType type,
|
||||||
|
TimeSpan? duration, List<LightlessNotificationAction>? actions, uint? soundEffectId)
|
||||||
|
{
|
||||||
|
return new LightlessNotification
|
||||||
|
{
|
||||||
|
Title = title,
|
||||||
|
Message = message,
|
||||||
|
Type = type,
|
||||||
|
Duration = duration ?? GetDefaultDurationForType(type),
|
||||||
|
Actions = actions ?? new List<LightlessNotificationAction>(),
|
||||||
|
SoundEffectId = GetSoundEffectId(type, soundEffectId),
|
||||||
|
ShowProgress = _configService.Current.ShowNotificationProgress,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WrapActionsWithAutoDismiss(LightlessNotification notification)
|
||||||
|
{
|
||||||
|
foreach (var action in notification.Actions)
|
||||||
|
{
|
||||||
|
var originalOnClick = action.OnClick;
|
||||||
|
action.OnClick = (n) =>
|
||||||
|
{
|
||||||
|
originalOnClick(n);
|
||||||
|
if (_configService.Current.AutoDismissOnAction)
|
||||||
|
{
|
||||||
|
DismissNotification(n);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ShowNotification(NotificationMessage msg)
|
private void DismissNotification(LightlessNotification notification)
|
||||||
{
|
{
|
||||||
Logger.LogInformation("{msg}", msg.ToString());
|
notification.IsDismissed = true;
|
||||||
|
notification.IsAnimatingOut = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowPairRequestNotification(string senderName, string senderId, Action onAccept, Action onDecline)
|
||||||
|
{
|
||||||
|
var location = GetNotificationLocation(NotificationType.PairRequest);
|
||||||
|
|
||||||
|
// Show in chat if configured
|
||||||
|
if (location == NotificationLocation.Chat || location == NotificationLocation.ChatAndLightlessUi)
|
||||||
|
{
|
||||||
|
ShowChat(new NotificationMessage("Pair Request Received", $"{senderName} wants to directly pair with you.", NotificationType.PairRequest));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show Lightless notification if configured and action buttons are enabled
|
||||||
|
if ((location == NotificationLocation.LightlessUi || location == NotificationLocation.ChatAndLightlessUi)
|
||||||
|
&& _configService.Current.UseLightlessNotifications
|
||||||
|
&& _configService.Current.ShowPairRequestNotificationActions)
|
||||||
|
{
|
||||||
|
var notification = new LightlessNotification
|
||||||
|
{
|
||||||
|
Id = $"pair_request_{senderId}",
|
||||||
|
Title = "Pair Request Received",
|
||||||
|
Message = $"{senderName} wants to directly pair with you.",
|
||||||
|
Type = NotificationType.PairRequest,
|
||||||
|
Duration = TimeSpan.FromSeconds(_configService.Current.PairRequestDurationSeconds),
|
||||||
|
SoundEffectId = GetPairRequestSoundId(),
|
||||||
|
Actions = CreatePairRequestActions(onAccept, onDecline)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (notification.SoundEffectId.HasValue)
|
||||||
|
{
|
||||||
|
PlayNotificationSound(notification.SoundEffectId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||||
|
}
|
||||||
|
else if (location != NotificationLocation.Nowhere && location != NotificationLocation.Chat)
|
||||||
|
{
|
||||||
|
// Fall back to regular notification without action buttons
|
||||||
|
HandleNotificationMessage(new NotificationMessage("Pair Request Received", $"{senderName} wants to directly pair with you.", NotificationType.PairRequest));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private uint? GetPairRequestSoundId() =>
|
||||||
|
!_configService.Current.DisablePairRequestSound ? _configService.Current.PairRequestSoundId : null;
|
||||||
|
|
||||||
|
private List<LightlessNotificationAction> CreatePairRequestActions(Action onAccept, Action onDecline)
|
||||||
|
{
|
||||||
|
return new List<LightlessNotificationAction>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = "accept",
|
||||||
|
Label = "Accept",
|
||||||
|
Icon = FontAwesomeIcon.Check,
|
||||||
|
Color = UIColors.Get("LightlessGreen"),
|
||||||
|
IsPrimary = true,
|
||||||
|
OnClick = (n) =>
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Pair request accepted");
|
||||||
|
onAccept();
|
||||||
|
DismissNotification(n);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = "decline",
|
||||||
|
Label = "Decline",
|
||||||
|
Icon = FontAwesomeIcon.Times,
|
||||||
|
Color = UIColors.Get("DimRed"),
|
||||||
|
IsDestructive = true,
|
||||||
|
OnClick = (n) =>
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Pair request declined");
|
||||||
|
onDecline();
|
||||||
|
DismissNotification(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowDownloadCompleteNotification(string fileName, int fileCount, Action? onOpenFolder = null)
|
||||||
|
{
|
||||||
|
var notification = new LightlessNotification
|
||||||
|
{
|
||||||
|
Title = "Download Complete",
|
||||||
|
Message = FormatDownloadCompleteMessage(fileName, fileCount),
|
||||||
|
Type = NotificationType.Info,
|
||||||
|
Duration = TimeSpan.FromSeconds(8),
|
||||||
|
Actions = CreateDownloadCompleteActions(onOpenFolder),
|
||||||
|
SoundEffectId = NotificationSounds.DownloadComplete
|
||||||
|
};
|
||||||
|
|
||||||
|
if (notification.SoundEffectId.HasValue)
|
||||||
|
{
|
||||||
|
PlayNotificationSound(notification.SoundEffectId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FormatDownloadCompleteMessage(string fileName, int fileCount) =>
|
||||||
|
fileCount > 1
|
||||||
|
? $"Downloaded {fileCount} files successfully."
|
||||||
|
: $"Downloaded {fileName} successfully.";
|
||||||
|
|
||||||
|
private List<LightlessNotificationAction> CreateDownloadCompleteActions(Action? onOpenFolder)
|
||||||
|
{
|
||||||
|
var actions = new List<LightlessNotificationAction>();
|
||||||
|
|
||||||
|
if (onOpenFolder != null)
|
||||||
|
{
|
||||||
|
actions.Add(new LightlessNotificationAction
|
||||||
|
{
|
||||||
|
Id = "open_folder",
|
||||||
|
Label = "Open Folder",
|
||||||
|
Icon = FontAwesomeIcon.FolderOpen,
|
||||||
|
Color = UIColors.Get("LightlessBlue"),
|
||||||
|
OnClick = (n) =>
|
||||||
|
{
|
||||||
|
onOpenFolder();
|
||||||
|
DismissNotification(n);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowErrorNotification(string title, string message, Exception? exception = null, Action? onRetry = null,
|
||||||
|
Action? onViewLog = null)
|
||||||
|
{
|
||||||
|
var notification = new LightlessNotification
|
||||||
|
{
|
||||||
|
Title = title,
|
||||||
|
Message = FormatErrorMessage(message, exception),
|
||||||
|
Type = NotificationType.Error,
|
||||||
|
Duration = TimeSpan.FromSeconds(15),
|
||||||
|
Actions = CreateErrorActions(onRetry, onViewLog),
|
||||||
|
SoundEffectId = NotificationSounds.Error
|
||||||
|
};
|
||||||
|
|
||||||
|
if (notification.SoundEffectId.HasValue)
|
||||||
|
{
|
||||||
|
PlayNotificationSound(notification.SoundEffectId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FormatErrorMessage(string message, Exception? exception) =>
|
||||||
|
exception != null ? $"{message}\n\nError: {exception.Message}" : message;
|
||||||
|
|
||||||
|
private List<LightlessNotificationAction> CreateErrorActions(Action? onRetry, Action? onViewLog)
|
||||||
|
{
|
||||||
|
var actions = new List<LightlessNotificationAction>();
|
||||||
|
|
||||||
|
if (onRetry != null)
|
||||||
|
{
|
||||||
|
actions.Add(new LightlessNotificationAction
|
||||||
|
{
|
||||||
|
Id = "retry",
|
||||||
|
Label = "Retry",
|
||||||
|
Icon = FontAwesomeIcon.Redo,
|
||||||
|
Color = UIColors.Get("LightlessBlue"),
|
||||||
|
OnClick = (n) =>
|
||||||
|
{
|
||||||
|
onRetry();
|
||||||
|
DismissNotification(n);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onViewLog != null)
|
||||||
|
{
|
||||||
|
actions.Add(new LightlessNotificationAction
|
||||||
|
{
|
||||||
|
Id = "view_log",
|
||||||
|
Label = "View Log",
|
||||||
|
Icon = FontAwesomeIcon.FileAlt,
|
||||||
|
Color = UIColors.Get("LightlessYellow"),
|
||||||
|
OnClick = (n) => onViewLog()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private string BuildPairDownloadMessage(List<(string PlayerName, float Progress, string Status)> userDownloads,
|
||||||
|
int queueWaiting)
|
||||||
|
{
|
||||||
|
var messageParts = new List<string>();
|
||||||
|
|
||||||
|
if (queueWaiting > 0)
|
||||||
|
{
|
||||||
|
messageParts.Add($"Queue: {queueWaiting} waiting");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userDownloads.Count > 0)
|
||||||
|
{
|
||||||
|
var completedCount = userDownloads.Count(x => x.Progress >= 1.0f);
|
||||||
|
messageParts.Add($"Progress: {completedCount}/{userDownloads.Count} completed");
|
||||||
|
}
|
||||||
|
|
||||||
|
var activeDownloadLines = BuildActiveDownloadLines(userDownloads);
|
||||||
|
if (!string.IsNullOrEmpty(activeDownloadLines))
|
||||||
|
{
|
||||||
|
messageParts.Add(activeDownloadLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join("\n", messageParts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BuildActiveDownloadLines(List<(string PlayerName, float Progress, string Status)> userDownloads)
|
||||||
|
{
|
||||||
|
var activeDownloads = userDownloads
|
||||||
|
.Where(x => x.Progress < 1.0f)
|
||||||
|
.Take(_configService.Current.MaxConcurrentPairApplications);
|
||||||
|
|
||||||
|
if (!activeDownloads.Any()) return string.Empty;
|
||||||
|
|
||||||
|
return string.Join("\n", activeDownloads.Select(x => $"• {x.PlayerName}: {FormatDownloadStatus(x)}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FormatDownloadStatus((string PlayerName, float Progress, string Status) download) =>
|
||||||
|
download.Status switch
|
||||||
|
{
|
||||||
|
"downloading" => $"{download.Progress:P0}",
|
||||||
|
"decompressing" => "decompressing",
|
||||||
|
"queued" => "queued",
|
||||||
|
"waiting" => "waiting for slot",
|
||||||
|
_ => download.Status
|
||||||
|
};
|
||||||
|
|
||||||
|
private bool AreAllDownloadsCompleted(List<(string PlayerName, float Progress, string Status)> userDownloads) =>
|
||||||
|
userDownloads.Any() && userDownloads.All(x => x.Progress >= 1.0f);
|
||||||
|
|
||||||
|
public void DismissPairDownloadNotification() =>
|
||||||
|
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
|
||||||
|
|
||||||
|
private TimeSpan GetDefaultDurationForType(NotificationType type) => type switch
|
||||||
|
{
|
||||||
|
NotificationType.Info => TimeSpan.FromSeconds(_configService.Current.InfoNotificationDurationSeconds),
|
||||||
|
NotificationType.Warning => TimeSpan.FromSeconds(_configService.Current.WarningNotificationDurationSeconds),
|
||||||
|
NotificationType.Error => TimeSpan.FromSeconds(_configService.Current.ErrorNotificationDurationSeconds),
|
||||||
|
NotificationType.PairRequest => TimeSpan.FromSeconds(_configService.Current.PairRequestDurationSeconds),
|
||||||
|
NotificationType.Download => TimeSpan.FromSeconds(_configService.Current.DownloadNotificationDurationSeconds),
|
||||||
|
NotificationType.Performance => TimeSpan.FromSeconds(_configService.Current.PerformanceNotificationDurationSeconds),
|
||||||
|
_ => TimeSpan.FromSeconds(10)
|
||||||
|
};
|
||||||
|
|
||||||
|
private uint? GetSoundEffectId(NotificationType type, uint? overrideSoundId)
|
||||||
|
{
|
||||||
|
if (overrideSoundId.HasValue) return overrideSoundId;
|
||||||
|
if (IsSoundDisabledForType(type)) return null;
|
||||||
|
return GetConfiguredSoundForType(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsSoundDisabledForType(NotificationType type) => type switch
|
||||||
|
{
|
||||||
|
NotificationType.Info => _configService.Current.DisableInfoSound,
|
||||||
|
NotificationType.Warning => _configService.Current.DisableWarningSound,
|
||||||
|
NotificationType.Error => _configService.Current.DisableErrorSound,
|
||||||
|
NotificationType.Performance => _configService.Current.DisablePerformanceSound,
|
||||||
|
NotificationType.Download => true, // Download sounds always disabled
|
||||||
|
_ => false
|
||||||
|
};
|
||||||
|
|
||||||
|
private uint GetConfiguredSoundForType(NotificationType type) => type switch
|
||||||
|
{
|
||||||
|
NotificationType.Info => _configService.Current.CustomInfoSoundId,
|
||||||
|
NotificationType.Warning => _configService.Current.CustomWarningSoundId,
|
||||||
|
NotificationType.Error => _configService.Current.CustomErrorSoundId,
|
||||||
|
NotificationType.Performance => _configService.Current.PerformanceSoundId,
|
||||||
|
_ => NotificationSounds.GetDefaultSound(type)
|
||||||
|
};
|
||||||
|
|
||||||
|
private void PlayNotificationSound(uint soundEffectId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
UIGlobals.PlayChatSoundEffect(soundEffectId);
|
||||||
|
_logger.LogDebug("Played notification sound effect {SoundId} via ChatGui", soundEffectId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to play notification sound effect {SoundId}", soundEffectId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleNotificationMessage(NotificationMessage msg)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("{msg}", msg.ToString());
|
||||||
if (!_dalamudUtilService.IsLoggedIn) return;
|
if (!_dalamudUtilService.IsLoggedIn) return;
|
||||||
|
|
||||||
switch (msg.Type)
|
var location = GetNotificationLocation(msg.Type);
|
||||||
{
|
ShowNotificationLocationBased(msg, location);
|
||||||
case NotificationType.Info:
|
|
||||||
ShowNotificationLocationBased(msg, _configurationService.Current.InfoNotification);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NotificationType.Warning:
|
|
||||||
ShowNotificationLocationBased(msg, _configurationService.Current.WarningNotification);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NotificationType.Error:
|
|
||||||
ShowNotificationLocationBased(msg, _configurationService.Current.ErrorNotification);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private NotificationLocation GetNotificationLocation(NotificationType type) =>
|
||||||
|
_configService.Current.UseLightlessNotifications
|
||||||
|
? GetLightlessNotificationLocation(type)
|
||||||
|
: GetClassicNotificationLocation(type);
|
||||||
|
|
||||||
|
private NotificationLocation GetLightlessNotificationLocation(NotificationType type) => type switch
|
||||||
|
{
|
||||||
|
NotificationType.Info => _configService.Current.LightlessInfoNotification,
|
||||||
|
NotificationType.Warning => _configService.Current.LightlessWarningNotification,
|
||||||
|
NotificationType.Error => _configService.Current.LightlessErrorNotification,
|
||||||
|
NotificationType.PairRequest => _configService.Current.LightlessPairRequestNotification,
|
||||||
|
NotificationType.Download => _configService.Current.LightlessDownloadNotification,
|
||||||
|
NotificationType.Performance => _configService.Current.LightlessPerformanceNotification,
|
||||||
|
_ => NotificationLocation.LightlessUi
|
||||||
|
};
|
||||||
|
|
||||||
|
private NotificationLocation GetClassicNotificationLocation(NotificationType type) => type switch
|
||||||
|
{
|
||||||
|
NotificationType.Info => _configService.Current.InfoNotification,
|
||||||
|
NotificationType.Warning => _configService.Current.WarningNotification,
|
||||||
|
NotificationType.Error => _configService.Current.ErrorNotification,
|
||||||
|
NotificationType.PairRequest => NotificationLocation.Toast,
|
||||||
|
NotificationType.Download => NotificationLocation.Toast,
|
||||||
|
_ => NotificationLocation.Nowhere
|
||||||
|
};
|
||||||
|
|
||||||
private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location)
|
private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location)
|
||||||
{
|
{
|
||||||
switch (location)
|
switch (location)
|
||||||
@@ -114,20 +450,29 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
ShowChat(msg);
|
ShowChat(msg);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case NotificationLocation.LightlessUi:
|
||||||
|
ShowLightlessNotification(msg);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NotificationLocation.ChatAndLightlessUi:
|
||||||
|
ShowChat(msg);
|
||||||
|
ShowLightlessNotification(msg);
|
||||||
|
break;
|
||||||
|
|
||||||
case NotificationLocation.Nowhere:
|
case NotificationLocation.Nowhere:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ShowLightlessNotification(NotificationMessage msg)
|
||||||
|
{
|
||||||
|
var duration = msg.TimeShownOnScreen ?? GetDefaultDurationForType(msg.Type);
|
||||||
|
ShowNotification(msg.Title ?? "Lightless Sync", msg.Message ?? string.Empty, msg.Type, duration, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
private void ShowToast(NotificationMessage msg)
|
private void ShowToast(NotificationMessage msg)
|
||||||
{
|
{
|
||||||
Dalamud.Interface.ImGuiNotification.NotificationType dalamudType = msg.Type switch
|
var dalamudType = ConvertToDalamudNotificationType(msg.Type);
|
||||||
{
|
|
||||||
NotificationType.Error => Dalamud.Interface.ImGuiNotification.NotificationType.Error,
|
|
||||||
NotificationType.Warning => Dalamud.Interface.ImGuiNotification.NotificationType.Warning,
|
|
||||||
NotificationType.Info => Dalamud.Interface.ImGuiNotification.NotificationType.Info,
|
|
||||||
_ => Dalamud.Interface.ImGuiNotification.NotificationType.Info
|
|
||||||
};
|
|
||||||
|
|
||||||
_notificationManager.AddNotification(new Notification()
|
_notificationManager.AddNotification(new Notification()
|
||||||
{
|
{
|
||||||
@@ -138,4 +483,274 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
InitialDuration = msg.TimeShownOnScreen ?? TimeSpan.FromSeconds(3)
|
InitialDuration = msg.TimeShownOnScreen ?? TimeSpan.FromSeconds(3)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Dalamud.Interface.ImGuiNotification.NotificationType
|
||||||
|
ConvertToDalamudNotificationType(NotificationType type) => type switch
|
||||||
|
{
|
||||||
|
NotificationType.Error => Dalamud.Interface.ImGuiNotification.NotificationType.Error,
|
||||||
|
NotificationType.Warning => Dalamud.Interface.ImGuiNotification.NotificationType.Warning,
|
||||||
|
_ => Dalamud.Interface.ImGuiNotification.NotificationType.Info
|
||||||
|
};
|
||||||
|
|
||||||
|
private void ShowChat(NotificationMessage msg)
|
||||||
|
{
|
||||||
|
switch (msg.Type)
|
||||||
|
{
|
||||||
|
case NotificationType.Info:
|
||||||
|
PrintInfoChat(msg.Message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NotificationType.Warning:
|
||||||
|
PrintWarnChat(msg.Message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NotificationType.Error:
|
||||||
|
PrintErrorChat(msg.Message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NotificationType.PairRequest:
|
||||||
|
PrintPairRequestChat(msg.Title, msg.Message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NotificationType.Performance:
|
||||||
|
PrintPerformanceChat(msg.Title, msg.Message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Download notifications don't support chat output, will be a giga spam otherwise
|
||||||
|
case NotificationType.Download:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PrintErrorChat(string? message)
|
||||||
|
{
|
||||||
|
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Error: " + message);
|
||||||
|
_chatGui.PrintError(se.BuiltString);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PrintInfoChat(string? message)
|
||||||
|
{
|
||||||
|
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] Info: ")
|
||||||
|
.AddItalics(message ?? string.Empty);
|
||||||
|
_chatGui.Print(se.BuiltString);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PrintWarnChat(string? message)
|
||||||
|
{
|
||||||
|
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ")
|
||||||
|
.AddUiForeground("Warning: " + (message ?? string.Empty), 31).AddUiForegroundOff();
|
||||||
|
_chatGui.Print(se.BuiltString);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PrintPairRequestChat(string? title, string? message)
|
||||||
|
{
|
||||||
|
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ")
|
||||||
|
.AddUiForeground("Pair Request: ", 541).AddUiForegroundOff()
|
||||||
|
.AddText(title ?? message ?? string.Empty);
|
||||||
|
_chatGui.Print(se.BuiltString);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PrintPerformanceChat(string? title, string? message)
|
||||||
|
{
|
||||||
|
SeStringBuilder se = new SeStringBuilder().AddText("[Lightless Sync] ")
|
||||||
|
.AddUiForeground("Performance: ", 508).AddUiForegroundOff()
|
||||||
|
.AddText(title ?? message ?? string.Empty);
|
||||||
|
_chatGui.Print(se.BuiltString);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandlePairRequestReceived(PairRequestReceivedMessage msg)
|
||||||
|
{
|
||||||
|
var request = _pairRequestService.RegisterIncomingRequest(msg.HashedCid, msg.Message);
|
||||||
|
var senderName = string.IsNullOrEmpty(request.DisplayName) ? "Unknown User" : request.DisplayName;
|
||||||
|
|
||||||
|
_shownPairRequestNotifications.Add(request.HashedCid);
|
||||||
|
ShowPairRequestNotification(
|
||||||
|
senderName,
|
||||||
|
request.HashedCid,
|
||||||
|
onAccept: () => _pairRequestService.AcceptPairRequest(request.HashedCid, senderName),
|
||||||
|
onDecline: () => _pairRequestService.DeclinePairRequest(request.HashedCid, senderName));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (expired)
|
||||||
|
var notificationsToRemove = _shownPairRequestNotifications
|
||||||
|
.Where(hashedCid => !activeRequestIds.Contains(hashedCid))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var hashedCid in notificationsToRemove)
|
||||||
|
{
|
||||||
|
var notificationId = $"pair_request_{hashedCid}";
|
||||||
|
Mediator.Publish(new LightlessNotificationDismissMessage(notificationId));
|
||||||
|
_shownPairRequestNotifications.Remove(hashedCid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandlePairDownloadStatus(PairDownloadStatusMessage msg)
|
||||||
|
{
|
||||||
|
var userDownloads = msg.DownloadStatus.Where(x => x.PlayerName != "Pair Queue").ToList();
|
||||||
|
var totalProgress = userDownloads.Count > 0 ? userDownloads.Average(x => x.Progress) : 0f;
|
||||||
|
var message = BuildPairDownloadMessage(userDownloads, msg.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 (userDownloads.Count == 0 || AreAllDownloadsCompleted(userDownloads))
|
||||||
|
{
|
||||||
|
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandlePerformanceNotification(PerformanceNotificationMessage msg)
|
||||||
|
{
|
||||||
|
var location = GetNotificationLocation(NotificationType.Performance);
|
||||||
|
|
||||||
|
// Show in chat if configured
|
||||||
|
if (location == NotificationLocation.Chat || location == NotificationLocation.ChatAndLightlessUi)
|
||||||
|
{
|
||||||
|
ShowChat(new NotificationMessage(msg.Title, msg.Message, NotificationType.Performance));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show Lightless notification if configured and action buttons are enabled
|
||||||
|
if ((location == NotificationLocation.LightlessUi || location == NotificationLocation.ChatAndLightlessUi)
|
||||||
|
&& _configService.Current.UseLightlessNotifications
|
||||||
|
&& _configService.Current.ShowPerformanceNotificationActions)
|
||||||
|
{
|
||||||
|
var actions = CreatePerformanceActions(msg.UserData, msg.IsPaused, msg.PlayerName);
|
||||||
|
var notification = new LightlessNotification
|
||||||
|
{
|
||||||
|
Title = msg.Title,
|
||||||
|
Message = msg.Message,
|
||||||
|
Type = NotificationType.Performance,
|
||||||
|
Duration = TimeSpan.FromSeconds(_configService.Current.PerformanceNotificationDurationSeconds),
|
||||||
|
Actions = actions,
|
||||||
|
SoundEffectId = GetSoundEffectId(NotificationType.Performance, null)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (notification.SoundEffectId.HasValue)
|
||||||
|
{
|
||||||
|
PlayNotificationSound(notification.SoundEffectId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||||
|
}
|
||||||
|
else if (location != NotificationLocation.Nowhere && location != NotificationLocation.Chat)
|
||||||
|
{
|
||||||
|
// Fall back to regular notification without action buttons
|
||||||
|
HandleNotificationMessage(new NotificationMessage(msg.Title, msg.Message, NotificationType.Performance));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<LightlessNotificationAction> CreatePerformanceActions(UserData userData, bool isPaused, string playerName)
|
||||||
|
{
|
||||||
|
var actions = new List<LightlessNotificationAction>();
|
||||||
|
|
||||||
|
if (isPaused)
|
||||||
|
{
|
||||||
|
actions.Add(new LightlessNotificationAction
|
||||||
|
{
|
||||||
|
Label = "Unpause",
|
||||||
|
Icon = FontAwesomeIcon.Play,
|
||||||
|
Color = UIColors.Get("LightlessGreen"),
|
||||||
|
IsPrimary = true,
|
||||||
|
OnClick = (notification) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Mediator.Publish(new CyclePauseMessage(userData));
|
||||||
|
DismissNotification(notification);
|
||||||
|
|
||||||
|
var displayName = GetUserDisplayName(userData, playerName);
|
||||||
|
ShowNotification(
|
||||||
|
"Player Unpaused",
|
||||||
|
$"Successfully unpaused {displayName}",
|
||||||
|
NotificationType.Info,
|
||||||
|
TimeSpan.FromSeconds(3));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to unpause player {uid}", userData.UID);
|
||||||
|
var displayName = GetUserDisplayName(userData, playerName);
|
||||||
|
ShowNotification(
|
||||||
|
"Unpause Failed",
|
||||||
|
$"Failed to unpause {displayName}",
|
||||||
|
NotificationType.Error,
|
||||||
|
TimeSpan.FromSeconds(5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
actions.Add(new LightlessNotificationAction
|
||||||
|
{
|
||||||
|
Label = "Pause",
|
||||||
|
Icon = FontAwesomeIcon.Pause,
|
||||||
|
Color = UIColors.Get("LightlessOrange"),
|
||||||
|
IsPrimary = true,
|
||||||
|
OnClick = (notification) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Mediator.Publish(new PauseMessage(userData));
|
||||||
|
DismissNotification(notification);
|
||||||
|
|
||||||
|
var displayName = GetUserDisplayName(userData, playerName);
|
||||||
|
ShowNotification(
|
||||||
|
"Player Paused",
|
||||||
|
$"Successfully paused {displayName}",
|
||||||
|
NotificationType.Info,
|
||||||
|
TimeSpan.FromSeconds(3));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to pause player {uid}", userData.UID);
|
||||||
|
var displayName = GetUserDisplayName(userData, playerName);
|
||||||
|
ShowNotification(
|
||||||
|
"Pause Failed",
|
||||||
|
$"Failed to pause {displayName}",
|
||||||
|
NotificationType.Error,
|
||||||
|
TimeSpan.FromSeconds(5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add dismiss button
|
||||||
|
actions.Add(new LightlessNotificationAction
|
||||||
|
{
|
||||||
|
Label = "Dismiss",
|
||||||
|
Icon = FontAwesomeIcon.Times,
|
||||||
|
Color = UIColors.Get("DimRed"),
|
||||||
|
IsPrimary = false,
|
||||||
|
OnClick = (notification) =>
|
||||||
|
{
|
||||||
|
DismissNotification(notification);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetUserDisplayName(UserData userData, string playerName)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(userData.Alias) && !string.Equals(userData.Alias, userData.UID, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return $"{playerName} ({userData.Alias})";
|
||||||
|
}
|
||||||
|
return $"{playerName} ({userData.UID})";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,7 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
|||||||
private readonly SemaphoreSlim _semaphore;
|
private readonly SemaphoreSlim _semaphore;
|
||||||
private int _currentLimit;
|
private int _currentLimit;
|
||||||
private int _pendingReductions;
|
private int _pendingReductions;
|
||||||
|
private int _pendingIncrements;
|
||||||
private int _waiting;
|
private int _waiting;
|
||||||
private int _inFlight;
|
private int _inFlight;
|
||||||
|
|
||||||
@@ -70,7 +71,7 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
if (!IsEnabled)
|
if (!IsEnabled)
|
||||||
{
|
{
|
||||||
_semaphore.Release();
|
TryReleaseSemaphore();
|
||||||
return NoopReleaser.Instance;
|
return NoopReleaser.Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,18 +91,12 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
|||||||
var releaseAmount = HardLimit - _semaphore.CurrentCount;
|
var releaseAmount = HardLimit - _semaphore.CurrentCount;
|
||||||
if (releaseAmount > 0)
|
if (releaseAmount > 0)
|
||||||
{
|
{
|
||||||
try
|
TryReleaseSemaphore(releaseAmount);
|
||||||
{
|
|
||||||
_semaphore.Release(releaseAmount);
|
|
||||||
}
|
|
||||||
catch (SemaphoreFullException)
|
|
||||||
{
|
|
||||||
// ignore, already at max
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_currentLimit = desiredLimit;
|
_currentLimit = desiredLimit;
|
||||||
_pendingReductions = 0;
|
_pendingReductions = 0;
|
||||||
|
_pendingIncrements = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,10 +108,13 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
|||||||
if (desiredLimit > _currentLimit)
|
if (desiredLimit > _currentLimit)
|
||||||
{
|
{
|
||||||
var increment = desiredLimit - _currentLimit;
|
var increment = desiredLimit - _currentLimit;
|
||||||
var allowed = Math.Min(increment, HardLimit - _semaphore.CurrentCount);
|
_pendingIncrements += increment;
|
||||||
if (allowed > 0)
|
|
||||||
|
var available = HardLimit - _semaphore.CurrentCount;
|
||||||
|
var toRelease = Math.Min(_pendingIncrements, available);
|
||||||
|
if (toRelease > 0 && TryReleaseSemaphore(toRelease))
|
||||||
{
|
{
|
||||||
_semaphore.Release(allowed);
|
_pendingIncrements -= toRelease;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -133,6 +131,13 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
_pendingReductions += remaining;
|
_pendingReductions += remaining;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_pendingIncrements > 0)
|
||||||
|
{
|
||||||
|
var offset = Math.Min(_pendingIncrements, _pendingReductions);
|
||||||
|
_pendingIncrements -= offset;
|
||||||
|
_pendingReductions -= offset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_currentLimit = desiredLimit;
|
_currentLimit = desiredLimit;
|
||||||
@@ -146,6 +151,25 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
|||||||
return Math.Clamp(configured, 1, HardLimit);
|
return Math.Clamp(configured, 1, HardLimit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool TryReleaseSemaphore(int count = 1)
|
||||||
|
{
|
||||||
|
if (count <= 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_semaphore.Release(count);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (SemaphoreFullException ex)
|
||||||
|
{
|
||||||
|
Logger.LogDebug(ex, "Attempted to release {count} pair processing slots but semaphore is already at the hard limit.", count);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void ReleaseOne()
|
private void ReleaseOne()
|
||||||
{
|
{
|
||||||
var inFlight = Interlocked.Decrement(ref _inFlight);
|
var inFlight = Interlocked.Decrement(ref _inFlight);
|
||||||
@@ -166,9 +190,20 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
|||||||
_pendingReductions--;
|
_pendingReductions--;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_pendingIncrements > 0)
|
||||||
|
{
|
||||||
|
if (!TryReleaseSemaphore())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_pendingIncrements--;
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_semaphore.Release();
|
TryReleaseSemaphore();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
using LightlessSync.PlayerData.Pairs;
|
using LightlessSync.PlayerData.Pairs;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -11,16 +13,23 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
private readonly DalamudUtilService _dalamudUtil;
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
private readonly PairManager _pairManager;
|
private readonly PairManager _pairManager;
|
||||||
|
private readonly Lazy<WebAPI.ApiController> _apiController;
|
||||||
private readonly object _syncRoot = new();
|
private readonly object _syncRoot = new();
|
||||||
private readonly List<PairRequestEntry> _requests = [];
|
private readonly List<PairRequestEntry> _requests = [];
|
||||||
|
|
||||||
private static readonly TimeSpan Expiration = TimeSpan.FromMinutes(5);
|
private static readonly TimeSpan Expiration = TimeSpan.FromMinutes(5);
|
||||||
|
|
||||||
public PairRequestService(ILogger<PairRequestService> logger, LightlessMediator mediator, DalamudUtilService dalamudUtil, PairManager pairManager)
|
public PairRequestService(
|
||||||
|
ILogger<PairRequestService> logger,
|
||||||
|
LightlessMediator mediator,
|
||||||
|
DalamudUtilService dalamudUtil,
|
||||||
|
PairManager pairManager,
|
||||||
|
Lazy<WebAPI.ApiController> apiController)
|
||||||
: base(logger, mediator)
|
: base(logger, mediator)
|
||||||
{
|
{
|
||||||
_dalamudUtil = dalamudUtil;
|
_dalamudUtil = dalamudUtil;
|
||||||
_pairManager = pairManager;
|
_pairManager = pairManager;
|
||||||
|
_apiController = apiController;
|
||||||
|
|
||||||
Mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, _ =>
|
Mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, _ =>
|
||||||
{
|
{
|
||||||
@@ -183,6 +192,44 @@ 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, string displayName)
|
||||||
|
{
|
||||||
|
RemoveRequest(hashedCid);
|
||||||
|
Mediator.Publish(new NotificationMessage("Pair request declined",
|
||||||
|
"Declined " + displayName + "'s pending pair request.",
|
||||||
|
NotificationType.Info,
|
||||||
|
TimeSpan.FromSeconds(3)));
|
||||||
|
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);
|
||||||
|
|||||||
@@ -78,23 +78,26 @@ public class PlayerPerformanceService
|
|||||||
string warningText = string.Empty;
|
string warningText = string.Empty;
|
||||||
if (exceedsTris && !exceedsVram)
|
if (exceedsTris && !exceedsVram)
|
||||||
{
|
{
|
||||||
warningText = $"Player {pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds your configured triangle warning threshold (" +
|
warningText = $"Player {pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds your configured triangle warning threshold\n" +
|
||||||
$"{triUsage}/{config.TrisWarningThresholdThousands * 1000} triangles).";
|
$"{triUsage}/{config.TrisWarningThresholdThousands * 1000} triangles";
|
||||||
}
|
}
|
||||||
else if (!exceedsTris)
|
else if (!exceedsTris)
|
||||||
{
|
{
|
||||||
warningText = $"Player {pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds your configured VRAM warning threshold (" +
|
warningText = $"Player {pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds your configured VRAM warning threshold\n" +
|
||||||
$"{UiSharedService.ByteToString(vramUsage, true)}/{config.VRAMSizeWarningThresholdMiB} MiB).";
|
$"{UiSharedService.ByteToString(vramUsage, true)}/{config.VRAMSizeWarningThresholdMiB} MiB";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
warningText = $"Player {pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds both VRAM warning threshold (" +
|
warningText = $"Player {pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds both VRAM warning threshold and triangle warning threshold\n" +
|
||||||
$"{UiSharedService.ByteToString(vramUsage, true)}/{config.VRAMSizeWarningThresholdMiB} MiB) and " +
|
$"{UiSharedService.ByteToString(vramUsage, true)}/{config.VRAMSizeWarningThresholdMiB} MiB and {triUsage}/{config.TrisWarningThresholdThousands * 1000} triangles";
|
||||||
$"triangle warning threshold ({triUsage}/{config.TrisWarningThresholdThousands * 1000} triangles).";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_mediator.Publish(new NotificationMessage($"{pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds performance threshold(s)",
|
_mediator.Publish(new PerformanceNotificationMessage(
|
||||||
warningText, LightlessConfiguration.Models.NotificationType.Warning));
|
$"{pairHandler.Pair.PlayerName} ({pairHandler.Pair.UserData.AliasOrUID}) exceeds performance threshold(s)",
|
||||||
|
warningText,
|
||||||
|
pairHandler.Pair.UserData,
|
||||||
|
pairHandler.Pair.IsPaused,
|
||||||
|
pairHandler.Pair.PlayerName));
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -138,11 +141,15 @@ public class PlayerPerformanceService
|
|||||||
if (CheckForThreshold(config.AutoPausePlayersExceedingThresholds, config.TrisAutoPauseThresholdThousands * 1000,
|
if (CheckForThreshold(config.AutoPausePlayersExceedingThresholds, config.TrisAutoPauseThresholdThousands * 1000,
|
||||||
triUsage, config.AutoPausePlayersWithPreferredPermissionsExceedingThresholds, isPrefPerm))
|
triUsage, config.AutoPausePlayersWithPreferredPermissionsExceedingThresholds, isPrefPerm))
|
||||||
{
|
{
|
||||||
_mediator.Publish(new NotificationMessage($"{pair.PlayerName} ({pair.UserData.AliasOrUID}) automatically paused",
|
var message = $"Player {pair.PlayerName} ({pair.UserData.AliasOrUID}) exceeded your configured triangle auto pause threshold and has been automatically paused\n" +
|
||||||
$"Player {pair.PlayerName} ({pair.UserData.AliasOrUID}) exceeded your configured triangle auto pause threshold (" +
|
$"{triUsage}/{config.TrisAutoPauseThresholdThousands * 1000} triangles";
|
||||||
$"{triUsage}/{config.TrisAutoPauseThresholdThousands * 1000} triangles)" +
|
|
||||||
$" and has been automatically paused.",
|
_mediator.Publish(new PerformanceNotificationMessage(
|
||||||
LightlessConfiguration.Models.NotificationType.Warning));
|
$"{pair.PlayerName} ({pair.UserData.AliasOrUID}) automatically paused",
|
||||||
|
message,
|
||||||
|
pair.UserData,
|
||||||
|
true,
|
||||||
|
pair.PlayerName));
|
||||||
|
|
||||||
_mediator.Publish(new EventMessage(new Event(pair.PlayerName, pair.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning,
|
_mediator.Publish(new EventMessage(new Event(pair.PlayerName, pair.UserData, nameof(PlayerPerformanceService), EventSeverity.Warning,
|
||||||
$"Exceeds triangle threshold: automatically paused ({triUsage}/{config.TrisAutoPauseThresholdThousands * 1000} triangles)")));
|
$"Exceeds triangle threshold: automatically paused ({triUsage}/{config.TrisAutoPauseThresholdThousands * 1000} triangles)")));
|
||||||
@@ -214,11 +221,15 @@ public class PlayerPerformanceService
|
|||||||
if (CheckForThreshold(config.AutoPausePlayersExceedingThresholds, config.VRAMSizeAutoPauseThresholdMiB * 1024 * 1024,
|
if (CheckForThreshold(config.AutoPausePlayersExceedingThresholds, config.VRAMSizeAutoPauseThresholdMiB * 1024 * 1024,
|
||||||
vramUsage, config.AutoPausePlayersWithPreferredPermissionsExceedingThresholds, isPrefPerm))
|
vramUsage, config.AutoPausePlayersWithPreferredPermissionsExceedingThresholds, isPrefPerm))
|
||||||
{
|
{
|
||||||
_mediator.Publish(new NotificationMessage($"{pair.PlayerName} ({pair.UserData.AliasOrUID}) automatically paused",
|
var message = $"Player {pair.PlayerName} ({pair.UserData.AliasOrUID}) exceeded your configured VRAM auto pause threshold and has been automatically paused\n" +
|
||||||
$"Player {pair.PlayerName} ({pair.UserData.AliasOrUID}) exceeded your configured VRAM auto pause threshold (" +
|
$"{UiSharedService.ByteToString(vramUsage, addSuffix: true)}/{config.VRAMSizeAutoPauseThresholdMiB}MiB";
|
||||||
$"{UiSharedService.ByteToString(vramUsage, addSuffix: true)}/{config.VRAMSizeAutoPauseThresholdMiB}MiB)" +
|
|
||||||
$" and has been automatically paused.",
|
_mediator.Publish(new PerformanceNotificationMessage(
|
||||||
LightlessConfiguration.Models.NotificationType.Warning));
|
$"{pair.PlayerName} ({pair.UserData.AliasOrUID}) automatically paused",
|
||||||
|
message,
|
||||||
|
pair.UserData,
|
||||||
|
true,
|
||||||
|
pair.PlayerName));
|
||||||
|
|
||||||
_mediator.Publish(new PauseMessage(pair.UserData));
|
_mediator.Publish(new PauseMessage(pair.UserData));
|
||||||
|
|
||||||
|
|||||||
@@ -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,8 +607,9 @@ public class ServerConfigurationManager
|
|||||||
{
|
{
|
||||||
var baseUri = serverUri.Replace("wss://", "https://").Replace("ws://", "http://");
|
var baseUri = serverUri.Replace("wss://", "https://").Replace("ws://", "http://");
|
||||||
var oauthCheckUri = LightlessAuth.GetUIDsFullPath(new Uri(baseUri));
|
var oauthCheckUri = LightlessAuth.GetUIDsFullPath(new Uri(baseUri));
|
||||||
_httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
using var request = new HttpRequestMessage(HttpMethod.Get, oauthCheckUri);
|
||||||
var response = await _httpClient.GetAsync(oauthCheckUri).ConfigureAwait(false);
|
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||||
|
using var response = await _httpClient.SendAsync(request).ConfigureAwait(false);
|
||||||
var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
|
var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||||
return await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(responseStream).ConfigureAwait(false) ?? [];
|
return await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(responseStream).ConfigureAwait(false) ?? [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using LightlessSync.API.Dto.Group;
|
using Dalamud.Interface.ImGuiFileDialog;
|
||||||
|
using LightlessSync.API.Dto.Group;
|
||||||
using LightlessSync.PlayerData.Pairs;
|
using LightlessSync.PlayerData.Pairs;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.Services.ServerConfiguration;
|
using LightlessSync.Services.ServerConfiguration;
|
||||||
@@ -18,10 +19,11 @@ public class UiFactory
|
|||||||
private readonly ServerConfigurationManager _serverConfigManager;
|
private readonly ServerConfigurationManager _serverConfigManager;
|
||||||
private readonly LightlessProfileManager _lightlessProfileManager;
|
private readonly LightlessProfileManager _lightlessProfileManager;
|
||||||
private readonly PerformanceCollectorService _performanceCollectorService;
|
private readonly PerformanceCollectorService _performanceCollectorService;
|
||||||
|
private readonly FileDialogManager _fileDialogManager;
|
||||||
|
|
||||||
public UiFactory(ILoggerFactory loggerFactory, LightlessMediator lightlessMediator, ApiController apiController,
|
public UiFactory(ILoggerFactory loggerFactory, LightlessMediator lightlessMediator, ApiController apiController,
|
||||||
UiSharedService uiSharedService, PairManager pairManager, ServerConfigurationManager serverConfigManager,
|
UiSharedService uiSharedService, PairManager pairManager, ServerConfigurationManager serverConfigManager,
|
||||||
LightlessProfileManager lightlessProfileManager, PerformanceCollectorService performanceCollectorService)
|
LightlessProfileManager lightlessProfileManager, PerformanceCollectorService performanceCollectorService, FileDialogManager fileDialogManager)
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
_lightlessMediator = lightlessMediator;
|
_lightlessMediator = lightlessMediator;
|
||||||
@@ -31,12 +33,13 @@ public class UiFactory
|
|||||||
_serverConfigManager = serverConfigManager;
|
_serverConfigManager = serverConfigManager;
|
||||||
_lightlessProfileManager = lightlessProfileManager;
|
_lightlessProfileManager = lightlessProfileManager;
|
||||||
_performanceCollectorService = performanceCollectorService;
|
_performanceCollectorService = performanceCollectorService;
|
||||||
|
_fileDialogManager = fileDialogManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SyncshellAdminUI CreateSyncshellAdminUi(GroupFullInfoDto dto)
|
public SyncshellAdminUI CreateSyncshellAdminUi(GroupFullInfoDto dto)
|
||||||
{
|
{
|
||||||
return new SyncshellAdminUI(_loggerFactory.CreateLogger<SyncshellAdminUI>(), _lightlessMediator,
|
return new SyncshellAdminUI(_loggerFactory.CreateLogger<SyncshellAdminUI>(), _lightlessMediator,
|
||||||
_apiController, _uiSharedService, _pairManager, dto, _performanceCollectorService);
|
_apiController, _uiSharedService, _pairManager, dto, _performanceCollectorService, _lightlessProfileManager, _fileDialogManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair)
|
public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair)
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.ImGuiFileDialog;
|
using Dalamud.Interface.ImGuiFileDialog;
|
||||||
using Dalamud.Interface.Windowing;
|
using Dalamud.Interface.Windowing;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.UI;
|
using LightlessSync.UI;
|
||||||
|
using LightlessSync.UI.Style;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace LightlessSync.Services;
|
namespace LightlessSync.Services;
|
||||||
@@ -119,7 +120,15 @@ public sealed class UiService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
private void Draw()
|
private void Draw()
|
||||||
{
|
{
|
||||||
_windowSystem.Draw();
|
MainStyle.PushStyle();
|
||||||
_fileDialogManager.Draw();
|
try
|
||||||
|
{
|
||||||
|
_windowSystem.Draw();
|
||||||
|
_fileDialogManager.Draw();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
MainStyle.PopStyle();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Colors;
|
using Dalamud.Interface.Colors;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
@@ -209,7 +210,7 @@ namespace LightlessSync.UI
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
|
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed"));
|
||||||
ImGui.Text("The Lightfinder<EFBFBD>s light wanes, but not in vain."); // cringe..
|
ImGui.Text("The Lightfinder’s light wanes, but not in vain."); // cringe..
|
||||||
ImGui.PopStyleColor();
|
ImGui.PopStyleColor();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,12 +253,27 @@ namespace LightlessSync.UI
|
|||||||
_broadcastService.ToggleBroadcast();
|
_broadcastService.ToggleBroadcast();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var toggleButtonHeight = ImGui.GetItemRectSize().Y;
|
||||||
|
|
||||||
if (isOnCooldown || !_broadcastService.IsLightFinderAvailable)
|
if (isOnCooldown || !_broadcastService.IsLightFinderAvailable)
|
||||||
ImGui.EndDisabled();
|
ImGui.EndDisabled();
|
||||||
|
|
||||||
ImGui.PopStyleColor();
|
ImGui.PopStyleColor();
|
||||||
ImGui.PopStyleVar();
|
ImGui.PopStyleVar();
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (_uiSharedService.IconButton(FontAwesomeIcon.Cog, toggleButtonHeight))
|
||||||
|
{
|
||||||
|
Mediator.Publish(new OpenLightfinderSettingsMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.BeginTooltip();
|
||||||
|
ImGui.TextUnformatted("Open Lightfinder settings.");
|
||||||
|
ImGui.EndTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
ImGui.EndTabItem();
|
ImGui.EndTabItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
@@ -87,7 +87,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
IpcManager ipcManager,
|
IpcManager ipcManager,
|
||||||
BroadcastService broadcastService,
|
BroadcastService broadcastService,
|
||||||
CharacterAnalyzer characterAnalyzer,
|
CharacterAnalyzer characterAnalyzer,
|
||||||
PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService)
|
PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService)
|
||||||
{
|
{
|
||||||
_uiSharedService = uiShared;
|
_uiSharedService = uiShared;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
@@ -105,7 +105,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
_renamePairTagUi = renameTagUi;
|
_renamePairTagUi = renameTagUi;
|
||||||
_ipcManager = ipcManager;
|
_ipcManager = ipcManager;
|
||||||
_broadcastService = broadcastService;
|
_broadcastService = broadcastService;
|
||||||
_tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService, dalamudUtilService);
|
_tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService, dalamudUtilService, lightlessNotificationService);
|
||||||
|
|
||||||
AllowPinning = true;
|
AllowPinning = true;
|
||||||
AllowClickthrough = false;
|
AllowClickthrough = false;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface.Colors;
|
using Dalamud.Interface.Colors;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
using LightlessSync.PlayerData.Handlers;
|
using LightlessSync.PlayerData.Handlers;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
@@ -22,9 +22,12 @@ 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 bool _notificationDismissed = true;
|
||||||
|
private int _lastDownloadStateHash = 0;
|
||||||
|
|
||||||
public DownloadUi(ILogger<DownloadUi> logger, DalamudUtilService dalamudUtilService, LightlessConfigService configService,
|
public DownloadUi(ILogger<DownloadUi> logger, DalamudUtilService dalamudUtilService, LightlessConfigService configService,
|
||||||
PairProcessingLimiter pairProcessingLimiter, FileUploadManager fileTransferManager, LightlessMediator mediator, UiSharedService uiShared, PerformanceCollectorService performanceCollectorService)
|
PairProcessingLimiter pairProcessingLimiter, FileUploadManager fileTransferManager, LightlessMediator mediator, UiSharedService uiShared,
|
||||||
|
PerformanceCollectorService performanceCollectorService)
|
||||||
: base(logger, mediator, "Lightless Sync Downloads", performanceCollectorService)
|
: base(logger, mediator, "Lightless Sync Downloads", performanceCollectorService)
|
||||||
{
|
{
|
||||||
_dalamudUtilService = dalamudUtilService;
|
_dalamudUtilService = dalamudUtilService;
|
||||||
@@ -56,7 +59,14 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
IsOpen = true;
|
IsOpen = true;
|
||||||
|
|
||||||
Mediator.Subscribe<DownloadStartedMessage>(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus);
|
Mediator.Subscribe<DownloadStartedMessage>(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus);
|
||||||
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _));
|
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
_currentDownloads.TryRemove(msg.DownloadId, out _);
|
||||||
|
if (!_currentDownloads.Any())
|
||||||
|
{
|
||||||
|
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
|
||||||
|
}
|
||||||
|
});
|
||||||
Mediator.Subscribe<GposeStartMessage>(this, (_) => IsOpen = false);
|
Mediator.Subscribe<GposeStartMessage>(this, (_) => IsOpen = false);
|
||||||
Mediator.Subscribe<GposeEndMessage>(this, (_) => IsOpen = true);
|
Mediator.Subscribe<GposeEndMessage>(this, (_) => IsOpen = true);
|
||||||
Mediator.Subscribe<PlayerUploadingMessage>(this, (msg) =>
|
Mediator.Subscribe<PlayerUploadingMessage>(this, (msg) =>
|
||||||
@@ -77,19 +87,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
if (_configService.Current.ShowTransferWindow)
|
if (_configService.Current.ShowTransferWindow)
|
||||||
{
|
{
|
||||||
var limiterSnapshot = _pairProcessingLimiter.GetSnapshot();
|
var limiterSnapshot = _pairProcessingLimiter.GetSnapshot();
|
||||||
if (limiterSnapshot.IsEnabled)
|
|
||||||
{
|
|
||||||
var queueColor = limiterSnapshot.Waiting > 0 ? ImGuiColors.DalamudYellow : ImGuiColors.DalamudGrey;
|
|
||||||
var queueText = $"Pair queue {limiterSnapshot.InFlight}/{limiterSnapshot.Limit}";
|
|
||||||
queueText += limiterSnapshot.Waiting > 0 ? $" ({limiterSnapshot.Waiting} waiting, {limiterSnapshot.Remaining} free)" : $" ({limiterSnapshot.Remaining} free)";
|
|
||||||
UiSharedService.DrawOutlinedFont(queueText, queueColor, new Vector4(0, 0, 0, 255), 1);
|
|
||||||
ImGui.NewLine();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
UiSharedService.DrawOutlinedFont("Pair apply limiter disabled", ImGuiColors.DalamudGrey, new Vector4(0, 0, 0, 255), 1);
|
|
||||||
ImGui.NewLine();
|
|
||||||
}
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_fileTransferManager.IsUploading)
|
if (_fileTransferManager.IsUploading)
|
||||||
@@ -117,38 +115,76 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// ignore errors thrown from UI
|
_logger.LogDebug("Error drawing upload progress");
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
foreach (var item in _currentDownloads.ToList())
|
// Check if download notifications are enabled (not set to TextOverlay)
|
||||||
|
var useNotifications = _configService.Current.UseLightlessNotifications
|
||||||
|
? _configService.Current.LightlessDownloadNotification != NotificationLocation.TextOverlay
|
||||||
|
: _configService.Current.UseNotificationsForDownloads;
|
||||||
|
|
||||||
|
if (useNotifications)
|
||||||
{
|
{
|
||||||
var dlSlot = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForSlot);
|
// Use notification system
|
||||||
var dlQueue = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForQueue);
|
if (_currentDownloads.Any())
|
||||||
var dlProg = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Downloading);
|
{
|
||||||
var dlDecomp = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Decompressing);
|
UpdateDownloadNotificationIfChanged(limiterSnapshot);
|
||||||
var totalFiles = item.Value.Sum(c => c.Value.TotalFiles);
|
_notificationDismissed = false;
|
||||||
var transferredFiles = item.Value.Sum(c => c.Value.TransferredFiles);
|
}
|
||||||
var totalBytes = item.Value.Sum(c => c.Value.TotalBytes);
|
else if (!_notificationDismissed)
|
||||||
var transferredBytes = item.Value.Sum(c => c.Value.TransferredBytes);
|
{
|
||||||
|
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
|
||||||
|
_notificationDismissed = true;
|
||||||
|
_lastDownloadStateHash = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Use text overlay
|
||||||
|
if (limiterSnapshot.IsEnabled)
|
||||||
|
{
|
||||||
|
var queueColor = limiterSnapshot.Waiting > 0 ? ImGuiColors.DalamudYellow : ImGuiColors.DalamudGrey;
|
||||||
|
var queueText = $"Pair queue {limiterSnapshot.InFlight}/{limiterSnapshot.Limit}";
|
||||||
|
queueText += limiterSnapshot.Waiting > 0 ? $" ({limiterSnapshot.Waiting} waiting, {limiterSnapshot.Remaining} free)" : $" ({limiterSnapshot.Remaining} free)";
|
||||||
|
UiSharedService.DrawOutlinedFont(queueText, queueColor, new Vector4(0, 0, 0, 255), 1);
|
||||||
|
ImGui.NewLine();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UiSharedService.DrawOutlinedFont("Pair apply limiter disabled", ImGuiColors.DalamudGrey, new Vector4(0, 0, 0, 255), 1);
|
||||||
|
ImGui.NewLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var item in _currentDownloads.ToList())
|
||||||
|
{
|
||||||
|
var dlSlot = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForSlot);
|
||||||
|
var dlQueue = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForQueue);
|
||||||
|
var dlProg = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Downloading);
|
||||||
|
var dlDecomp = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Decompressing);
|
||||||
|
var totalFiles = item.Value.Sum(c => c.Value.TotalFiles);
|
||||||
|
var transferredFiles = item.Value.Sum(c => c.Value.TransferredFiles);
|
||||||
|
var totalBytes = item.Value.Sum(c => c.Value.TotalBytes);
|
||||||
|
var transferredBytes = item.Value.Sum(c => c.Value.TransferredBytes);
|
||||||
|
|
||||||
UiSharedService.DrawOutlinedFont($"▼", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
|
UiSharedService.DrawOutlinedFont($"▼", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
var xDistance = ImGui.GetCursorPosX();
|
var xDistance = ImGui.GetCursorPosX();
|
||||||
UiSharedService.DrawOutlinedFont(
|
UiSharedService.DrawOutlinedFont(
|
||||||
$"{item.Key.Name} [W:{dlSlot}/Q:{dlQueue}/P:{dlProg}/D:{dlDecomp}]",
|
$"{item.Key.Name} [W:{dlSlot}/Q:{dlQueue}/P:{dlProg}/D:{dlDecomp}]",
|
||||||
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
|
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
|
||||||
ImGui.NewLine();
|
ImGui.NewLine();
|
||||||
ImGui.SameLine(xDistance);
|
ImGui.SameLine(xDistance);
|
||||||
UiSharedService.DrawOutlinedFont(
|
UiSharedService.DrawOutlinedFont(
|
||||||
$"{transferredFiles}/{totalFiles} ({UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)})",
|
$"{transferredFiles}/{totalFiles} ({UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)})",
|
||||||
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
|
ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// ignore errors thrown from UI
|
_logger.LogDebug("Error drawing download progress");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,7 +256,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// ignore errors thrown on UI
|
_logger.LogDebug("Error drawing upload progress");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,4 +298,68 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
MaximumSize = new Vector2(300, maxHeight),
|
MaximumSize = new Vector2(300, maxHeight),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateDownloadNotificationIfChanged(PairProcessingLimiterSnapshot limiterSnapshot)
|
||||||
|
{
|
||||||
|
var downloadStatus = new List<(string playerName, float progress, string status)>(_currentDownloads.Count);
|
||||||
|
var hashCode = new HashCode();
|
||||||
|
|
||||||
|
foreach (var item in _currentDownloads)
|
||||||
|
{
|
||||||
|
var dlSlot = 0;
|
||||||
|
var dlQueue = 0;
|
||||||
|
var dlProg = 0;
|
||||||
|
var dlDecomp = 0;
|
||||||
|
long totalBytes = 0;
|
||||||
|
long transferredBytes = 0;
|
||||||
|
|
||||||
|
// Single pass through the dictionary to count everything - avoid multiple LINQ iterations
|
||||||
|
foreach (var entry in item.Value)
|
||||||
|
{
|
||||||
|
var fileStatus = entry.Value;
|
||||||
|
switch (fileStatus.DownloadStatus)
|
||||||
|
{
|
||||||
|
case DownloadStatus.WaitingForSlot: dlSlot++; break;
|
||||||
|
case DownloadStatus.WaitingForQueue: dlQueue++; break;
|
||||||
|
case DownloadStatus.Downloading: dlProg++; break;
|
||||||
|
case DownloadStatus.Decompressing: dlDecomp++; break;
|
||||||
|
}
|
||||||
|
totalBytes += fileStatus.TotalBytes;
|
||||||
|
transferredBytes += fileStatus.TransferredBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
var progress = totalBytes > 0 ? (float)transferredBytes / totalBytes : 0f;
|
||||||
|
|
||||||
|
string status;
|
||||||
|
if (dlDecomp > 0) status = "decompressing";
|
||||||
|
else if (dlProg > 0) status = "downloading";
|
||||||
|
else if (dlQueue > 0) status = "queued";
|
||||||
|
else if (dlSlot > 0) status = "waiting";
|
||||||
|
else status = "completed";
|
||||||
|
|
||||||
|
downloadStatus.Add((item.Key.Name, progress, status));
|
||||||
|
|
||||||
|
// Build hash from meaningful state
|
||||||
|
hashCode.Add(item.Key.Name);
|
||||||
|
hashCode.Add(transferredBytes);
|
||||||
|
hashCode.Add(totalBytes);
|
||||||
|
hashCode.Add(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
var queueWaiting = limiterSnapshot.IsEnabled ? limiterSnapshot.Waiting : 0;
|
||||||
|
hashCode.Add(queueWaiting);
|
||||||
|
|
||||||
|
var currentHash = hashCode.ToHashCode();
|
||||||
|
|
||||||
|
// Only update notification if state has actually changed
|
||||||
|
if (currentHash != _lastDownloadStateHash)
|
||||||
|
{
|
||||||
|
_lastDownloadStateHash = currentHash;
|
||||||
|
if (downloadStatus.Count > 0 || queueWaiting > 0)
|
||||||
|
{
|
||||||
|
Mediator.Publish(new PairDownloadStatusMessage(downloadStatus, queueWaiting));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,56 +1,92 @@
|
|||||||
using Dalamud.Game.Gui.Dtr;
|
using Dalamud.Game.Gui.Dtr;
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.LightlessConfiguration.Configurations;
|
using LightlessSync.LightlessConfiguration.Configurations;
|
||||||
using LightlessSync.PlayerData.Pairs;
|
using LightlessSync.PlayerData.Pairs;
|
||||||
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.Services.ServerConfiguration;
|
using LightlessSync.Services.ServerConfiguration;
|
||||||
using LightlessSync.WebAPI;
|
using LightlessSync.WebAPI;
|
||||||
using LightlessSync.WebAPI.SignalR.Utils;
|
using LightlessSync.WebAPI.SignalR.Utils;
|
||||||
|
using LightlessSync.Utils;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace LightlessSync.UI;
|
namespace LightlessSync.UI;
|
||||||
|
|
||||||
public sealed class DtrEntry : IDisposable, IHostedService
|
public sealed class DtrEntry : IDisposable, IHostedService
|
||||||
{
|
{
|
||||||
|
private static readonly TimeSpan _localHashedCidCacheDuration = TimeSpan.FromMinutes(2);
|
||||||
|
private static readonly TimeSpan _localHashedCidErrorCooldown = TimeSpan.FromMinutes(1);
|
||||||
|
|
||||||
private readonly ApiController _apiController;
|
private readonly ApiController _apiController;
|
||||||
private readonly ServerConfigurationManager _serverManager;
|
private readonly ServerConfigurationManager _serverManager;
|
||||||
private readonly CancellationTokenSource _cancellationTokenSource = new();
|
private readonly CancellationTokenSource _cancellationTokenSource = new();
|
||||||
private readonly ConfigurationServiceBase<LightlessConfig> _configService;
|
private readonly ConfigurationServiceBase<LightlessConfig> _configService;
|
||||||
private readonly IDtrBar _dtrBar;
|
private readonly IDtrBar _dtrBar;
|
||||||
private readonly Lazy<IDtrBarEntry> _entry;
|
private readonly Lazy<IDtrBarEntry> _statusEntry;
|
||||||
|
private readonly Lazy<IDtrBarEntry> _lightfinderEntry;
|
||||||
private readonly ILogger<DtrEntry> _logger;
|
private readonly ILogger<DtrEntry> _logger;
|
||||||
|
private readonly BroadcastService _broadcastService;
|
||||||
|
private readonly BroadcastScannerService _broadcastScannerService;
|
||||||
private readonly LightlessMediator _lightlessMediator;
|
private readonly LightlessMediator _lightlessMediator;
|
||||||
private readonly PairManager _pairManager;
|
private readonly PairManager _pairManager;
|
||||||
|
private readonly PairRequestService _pairRequestService;
|
||||||
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
private Task? _runTask;
|
private Task? _runTask;
|
||||||
private string? _text;
|
private string? _statusText;
|
||||||
private string? _tooltip;
|
private string? _statusTooltip;
|
||||||
private Colors _colors;
|
private Colors _statusColors;
|
||||||
|
private string? _lightfinderText;
|
||||||
|
private string? _lightfinderTooltip;
|
||||||
|
private Colors _lightfinderColors;
|
||||||
|
private string? _localHashedCid;
|
||||||
|
private DateTime _localHashedCidFetchedAt = DateTime.MinValue;
|
||||||
|
private DateTime _localHashedCidNextErrorLog = DateTime.MinValue;
|
||||||
|
private DateTime _pairRequestNextErrorLog = DateTime.MinValue;
|
||||||
|
|
||||||
public DtrEntry(ILogger<DtrEntry> logger, IDtrBar dtrBar, ConfigurationServiceBase<LightlessConfig> configService, LightlessMediator lightlessMediator, PairManager pairManager, ApiController apiController, ServerConfigurationManager serverManager)
|
public DtrEntry(
|
||||||
|
ILogger<DtrEntry> logger,
|
||||||
|
IDtrBar dtrBar,
|
||||||
|
ConfigurationServiceBase<LightlessConfig> configService,
|
||||||
|
LightlessMediator lightlessMediator,
|
||||||
|
PairManager pairManager,
|
||||||
|
PairRequestService pairRequestService,
|
||||||
|
ApiController apiController,
|
||||||
|
ServerConfigurationManager serverManager,
|
||||||
|
BroadcastService broadcastService,
|
||||||
|
BroadcastScannerService broadcastScannerService,
|
||||||
|
DalamudUtilService dalamudUtilService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_dtrBar = dtrBar;
|
_dtrBar = dtrBar;
|
||||||
_entry = new(CreateEntry);
|
_statusEntry = new(CreateStatusEntry);
|
||||||
|
_lightfinderEntry = new(CreateLightfinderEntry);
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_lightlessMediator = lightlessMediator;
|
_lightlessMediator = lightlessMediator;
|
||||||
_pairManager = pairManager;
|
_pairManager = pairManager;
|
||||||
|
_pairRequestService = pairRequestService;
|
||||||
_apiController = apiController;
|
_apiController = apiController;
|
||||||
_serverManager = serverManager;
|
_serverManager = serverManager;
|
||||||
|
_broadcastService = broadcastService;
|
||||||
|
_broadcastScannerService = broadcastScannerService;
|
||||||
|
_dalamudUtilService = dalamudUtilService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (_entry.IsValueCreated)
|
if (_statusEntry.IsValueCreated)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Disposing DtrEntry");
|
_logger.LogDebug("Disposing DtrEntry");
|
||||||
Clear();
|
Clear();
|
||||||
_entry.Value.Remove();
|
_statusEntry.Value.Remove();
|
||||||
}
|
}
|
||||||
|
if (_lightfinderEntry.IsValueCreated)
|
||||||
|
_lightfinderEntry.Value.Remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
@@ -70,7 +106,7 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
|||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
// ignore cancelled
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -80,33 +116,66 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
|||||||
|
|
||||||
private void Clear()
|
private void Clear()
|
||||||
{
|
{
|
||||||
if (!_entry.IsValueCreated) return;
|
HideStatusEntry();
|
||||||
_logger.LogInformation("Clearing entry");
|
HideLightfinderEntry();
|
||||||
_text = null;
|
|
||||||
_tooltip = null;
|
|
||||||
_colors = default;
|
|
||||||
|
|
||||||
_entry.Value.Shown = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private IDtrBarEntry CreateEntry()
|
private void HideStatusEntry()
|
||||||
{
|
{
|
||||||
_logger.LogTrace("Creating new DtrBar entry");
|
if (_statusEntry.IsValueCreated && _statusEntry.Value.Shown)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Hiding status entry");
|
||||||
|
_statusEntry.Value.Shown = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_statusText = null;
|
||||||
|
_statusTooltip = null;
|
||||||
|
_statusColors = default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HideLightfinderEntry()
|
||||||
|
{
|
||||||
|
if (_lightfinderEntry.IsValueCreated && _lightfinderEntry.Value.Shown)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Hiding Lightfinder entry");
|
||||||
|
_lightfinderEntry.Value.Shown = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lightfinderText = null;
|
||||||
|
_lightfinderTooltip = null;
|
||||||
|
_lightfinderColors = default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IDtrBarEntry CreateStatusEntry()
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Creating status DtrBar entry");
|
||||||
var entry = _dtrBar.Get("Lightless Sync");
|
var entry = _dtrBar.Get("Lightless Sync");
|
||||||
entry.OnClick = interactionEvent => OnClickEvent(interactionEvent);
|
entry.OnClick = interactionEvent => OnStatusEntryClick(interactionEvent);
|
||||||
|
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnClickEvent(DtrInteractionEvent interactionEvent)
|
private IDtrBarEntry CreateLightfinderEntry()
|
||||||
{
|
{
|
||||||
if (interactionEvent.ClickType.Equals(MouseClickType.Left) && !interactionEvent.ModifierKeys.Equals(ClickModifierKeys.Shift))
|
_logger.LogTrace("Creating Lightfinder DtrBar entry");
|
||||||
|
var entry = _dtrBar.Get("Lightfinder");
|
||||||
|
entry.OnClick = interactionEvent => OnLightfinderEntryClick(interactionEvent);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnStatusEntryClick(DtrInteractionEvent interactionEvent)
|
||||||
|
{
|
||||||
|
if (interactionEvent.ClickType.Equals(MouseClickType.Left))
|
||||||
{
|
{
|
||||||
_lightlessMediator.Publish(new UiToggleMessage(typeof(CompactUi)));
|
if (interactionEvent.ModifierKeys.HasFlag(ClickModifierKeys.Shift))
|
||||||
}
|
{
|
||||||
else if (interactionEvent.ClickType.Equals(MouseClickType.Left) && interactionEvent.ModifierKeys.Equals(ClickModifierKeys.Shift))
|
_lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
|
||||||
{
|
}
|
||||||
_lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
|
else
|
||||||
|
{
|
||||||
|
_lightlessMediator.Publish(new UiToggleMessage(typeof(CompactUi)));
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (interactionEvent.ClickType.Equals(MouseClickType.Right))
|
if (interactionEvent.ClickType.Equals(MouseClickType.Right))
|
||||||
@@ -131,6 +200,17 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnLightfinderEntryClick(DtrInteractionEvent interactionEvent)
|
||||||
|
{
|
||||||
|
if (!_configService.Current.ShowLightfinderInDtr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (interactionEvent.ClickType.Equals(MouseClickType.Left))
|
||||||
|
{
|
||||||
|
_broadcastService.ToggleBroadcast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task RunAsync()
|
private async Task RunAsync()
|
||||||
{
|
{
|
||||||
while (!_cancellationTokenSource.IsCancellationRequested)
|
while (!_cancellationTokenSource.IsCancellationRequested)
|
||||||
@@ -143,96 +223,278 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
|||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
{
|
{
|
||||||
if (!_configService.Current.EnableDtrEntry || !_configService.Current.HasValidSetup())
|
var config = _configService.Current;
|
||||||
{
|
|
||||||
if (_entry.IsValueCreated && _entry.Value.Shown)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Disabling entry");
|
|
||||||
|
|
||||||
Clear();
|
if (!config.HasValidSetup())
|
||||||
}
|
{
|
||||||
|
HideStatusEntry();
|
||||||
|
HideLightfinderEntry();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_entry.Value.Shown)
|
if (config.EnableDtrEntry)
|
||||||
{
|
UpdateStatusEntry(config);
|
||||||
_logger.LogInformation("Showing entry");
|
else
|
||||||
_entry.Value.Shown = true;
|
HideStatusEntry();
|
||||||
}
|
|
||||||
|
|
||||||
|
if (config.ShowLightfinderInDtr)
|
||||||
|
UpdateLightfinderEntry(config);
|
||||||
|
else
|
||||||
|
HideLightfinderEntry();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateStatusEntry(LightlessConfig config)
|
||||||
|
{
|
||||||
string text;
|
string text;
|
||||||
string tooltip;
|
string tooltip;
|
||||||
Colors colors;
|
Colors colors;
|
||||||
|
|
||||||
if (_apiController.IsConnected)
|
if (_apiController.IsConnected)
|
||||||
{
|
{
|
||||||
var pairCount = _pairManager.GetVisibleUserCount();
|
var pairCount = _pairManager.GetVisibleUserCount();
|
||||||
text = $"\uE044 {pairCount}";
|
text = $"\uE044 {pairCount}";
|
||||||
if (pairCount > 0)
|
if (pairCount > 0)
|
||||||
{
|
{
|
||||||
IEnumerable<string> visiblePairs;
|
var preferNote = config.PreferNoteInDtrTooltip;
|
||||||
if (_configService.Current.ShowUidInDtrTooltip)
|
var showUid = config.ShowUidInDtrTooltip;
|
||||||
{
|
|
||||||
visiblePairs = _pairManager.GetOnlineUserPairs()
|
var visiblePairsQuery = _pairManager.GetOnlineUserPairs()
|
||||||
.Where(x => x.IsVisible)
|
.Where(x => x.IsVisible);
|
||||||
.Select(x => string.Format("{0} ({1})", _configService.Current.PreferNoteInDtrTooltip ? x.GetNote() ?? x.PlayerName : x.PlayerName, x.UserData.AliasOrUID));
|
|
||||||
}
|
IEnumerable<string> visiblePairs = showUid
|
||||||
else
|
? visiblePairsQuery.Select(x => string.Format("{0} ({1})", preferNote ? x.GetNote() ?? x.PlayerName : x.PlayerName, x.UserData.AliasOrUID))
|
||||||
{
|
: visiblePairsQuery.Select(x => string.Format("{0}", preferNote ? x.GetNote() ?? x.PlayerName : x.PlayerName));
|
||||||
visiblePairs = _pairManager.GetOnlineUserPairs()
|
|
||||||
.Where(x => x.IsVisible)
|
|
||||||
.Select(x => string.Format("{0}", _configService.Current.PreferNoteInDtrTooltip ? x.GetNote() ?? x.PlayerName : x.PlayerName));
|
|
||||||
}
|
|
||||||
|
|
||||||
tooltip = $"Lightless Sync: Connected{Environment.NewLine}----------{Environment.NewLine}{string.Join(Environment.NewLine, visiblePairs)}";
|
tooltip = $"Lightless Sync: Connected{Environment.NewLine}----------{Environment.NewLine}{string.Join(Environment.NewLine, visiblePairs)}";
|
||||||
colors = _configService.Current.DtrColorsPairsInRange;
|
colors = config.DtrColorsPairsInRange;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
tooltip = "Lightless Sync: Connected";
|
tooltip = "Lightless Sync: Connected";
|
||||||
colors = _configService.Current.DtrColorsDefault;
|
colors = config.DtrColorsDefault;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
text = "\uE044 \uE04C";
|
text = "\uE044 \uE04C";
|
||||||
tooltip = "Lightless Sync: Not Connected";
|
tooltip = "Lightless Sync: Not Connected";
|
||||||
colors = _configService.Current.DtrColorsNotConnected;
|
colors = config.DtrColorsNotConnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_configService.Current.UseColorsInDtr)
|
if (!config.UseColorsInDtr)
|
||||||
colors = default;
|
colors = default;
|
||||||
|
|
||||||
if (!string.Equals(text, _text, StringComparison.Ordinal) || !string.Equals(tooltip, _tooltip, StringComparison.Ordinal) || colors != _colors)
|
var statusEntry = _statusEntry.Value;
|
||||||
|
if (!statusEntry.Shown)
|
||||||
{
|
{
|
||||||
_text = text;
|
_logger.LogInformation("Showing status entry");
|
||||||
_tooltip = tooltip;
|
statusEntry.Shown = true;
|
||||||
_colors = colors;
|
|
||||||
_entry.Value.Text = BuildColoredSeString(text, colors);
|
|
||||||
_entry.Value.Tooltip = tooltip;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool statusNeedsUpdate =
|
||||||
|
!string.Equals(text, _statusText, StringComparison.Ordinal) ||
|
||||||
|
!string.Equals(tooltip, _statusTooltip, StringComparison.Ordinal) ||
|
||||||
|
colors != _statusColors;
|
||||||
|
|
||||||
|
if (statusNeedsUpdate)
|
||||||
|
{
|
||||||
|
statusEntry.Text = BuildColoredSeString(text, colors);
|
||||||
|
statusEntry.Tooltip = tooltip;
|
||||||
|
_statusText = text;
|
||||||
|
_statusTooltip = tooltip;
|
||||||
|
_statusColors = colors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateLightfinderEntry(LightlessConfig config)
|
||||||
|
{
|
||||||
|
var lightfinderEntry = _lightfinderEntry.Value;
|
||||||
|
if (!lightfinderEntry.Shown)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Showing Lightfinder entry");
|
||||||
|
lightfinderEntry.Shown = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var indicator = BuildLightfinderIndicator();
|
||||||
|
var lightfinderText = indicator.Text ?? string.Empty;
|
||||||
|
var lightfinderColors = config.UseLightfinderColorsInDtr ? indicator.Colors : default;
|
||||||
|
var lightfinderTooltip = BuildLightfinderTooltip(indicator.Tooltip);
|
||||||
|
|
||||||
|
bool lightfinderNeedsUpdate =
|
||||||
|
!string.Equals(lightfinderText, _lightfinderText, StringComparison.Ordinal) ||
|
||||||
|
!string.Equals(lightfinderTooltip, _lightfinderTooltip, StringComparison.Ordinal) ||
|
||||||
|
lightfinderColors != _lightfinderColors;
|
||||||
|
|
||||||
|
if (lightfinderNeedsUpdate)
|
||||||
|
{
|
||||||
|
lightfinderEntry.Text = BuildColoredSeString(lightfinderText, lightfinderColors);
|
||||||
|
lightfinderEntry.Tooltip = lightfinderTooltip;
|
||||||
|
_lightfinderText = lightfinderText;
|
||||||
|
_lightfinderTooltip = lightfinderTooltip;
|
||||||
|
_lightfinderColors = lightfinderColors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? GetLocalHashedCid()
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if (_localHashedCid is not null && now - _localHashedCidFetchedAt < _localHashedCidCacheDuration)
|
||||||
|
return _localHashedCid;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cid = _dalamudUtilService.GetCIDAsync().GetAwaiter().GetResult();
|
||||||
|
var hashedCid = cid.ToString().GetHash256();
|
||||||
|
_localHashedCid = hashedCid;
|
||||||
|
_localHashedCidFetchedAt = now;
|
||||||
|
return hashedCid;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (now >= _localHashedCidNextErrorLog)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Failed to refresh local hashed CID for Lightfinder DTR entry.");
|
||||||
|
_localHashedCidNextErrorLog = now + _localHashedCidErrorCooldown;
|
||||||
|
}
|
||||||
|
|
||||||
|
_localHashedCid = null;
|
||||||
|
_localHashedCidFetchedAt = now;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetNearbyBroadcastCount()
|
||||||
|
{
|
||||||
|
var localHashedCid = GetLocalHashedCid();
|
||||||
|
return _broadcastScannerService.CountActiveBroadcasts(
|
||||||
|
string.IsNullOrEmpty(localHashedCid) ? null : localHashedCid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetPendingPairRequestCount()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return _pairRequestService.GetActiveRequests().Count;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if (now >= _pairRequestNextErrorLog)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Failed to retrieve pair request count for Lightfinder DTR entry.");
|
||||||
|
_pairRequestNextErrorLog = now + _localHashedCidErrorCooldown;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private (string Text, Colors Colors, string Tooltip) BuildLightfinderIndicator()
|
||||||
|
{
|
||||||
|
var config = _configService.Current;
|
||||||
|
const string icon = "\uE048";
|
||||||
|
if (!_broadcastService.IsLightFinderAvailable)
|
||||||
|
{
|
||||||
|
return ($"{icon} --", SwapColorChannels(config.DtrColorsLightfinderUnavailable), "Lightfinder - Unavailable on this server.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_broadcastService.IsBroadcasting)
|
||||||
|
{
|
||||||
|
var tooltipBuilder = new StringBuilder("Lightfinder - Enabled");
|
||||||
|
|
||||||
|
switch (config.LightfinderDtrDisplayMode)
|
||||||
|
{
|
||||||
|
case LightfinderDtrDisplayMode.PendingPairRequests:
|
||||||
|
{
|
||||||
|
var requestCount = GetPendingPairRequestCount();
|
||||||
|
tooltipBuilder.AppendLine();
|
||||||
|
tooltipBuilder.Append("Pending pair requests: ").Append(requestCount);
|
||||||
|
return ($"{icon} Requests {requestCount}", SwapColorChannels(config.DtrColorsLightfinderEnabled), tooltipBuilder.ToString());
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
var broadcastCount = GetNearbyBroadcastCount();
|
||||||
|
tooltipBuilder.AppendLine();
|
||||||
|
tooltipBuilder.Append("Nearby Lightfinder users: ").Append(broadcastCount);
|
||||||
|
return ($"{icon} {broadcastCount}", SwapColorChannels(config.DtrColorsLightfinderEnabled), tooltipBuilder.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var tooltip = new StringBuilder("Lightfinder - Disabled");
|
||||||
|
var colors = SwapColorChannels(config.DtrColorsLightfinderDisabled);
|
||||||
|
if (_broadcastService.RemainingCooldown is { } cooldown && cooldown > TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
tooltip.AppendLine();
|
||||||
|
tooltip.Append("Cooldown: ").Append(Math.Ceiling(cooldown.TotalSeconds)).Append("s");
|
||||||
|
colors = SwapColorChannels(config.DtrColorsLightfinderCooldown);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ($"{icon} OFF", colors, tooltip.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildLightfinderTooltip(string baseTooltip)
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
if (!string.IsNullOrWhiteSpace(baseTooltip))
|
||||||
|
builder.Append(baseTooltip.TrimEnd());
|
||||||
|
else
|
||||||
|
builder.Append("Lightfinder status unavailable.");
|
||||||
|
|
||||||
|
return builder.ToString().TrimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendColoredSegment(SeStringBuilder builder, string? text, Colors colors)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (colors.Foreground != default)
|
||||||
|
builder.Add(BuildColorStartPayload(_colorTypeForeground, colors.Foreground));
|
||||||
|
if (colors.Glow != default)
|
||||||
|
builder.Add(BuildColorStartPayload(_colorTypeGlow, colors.Glow));
|
||||||
|
|
||||||
|
builder.AddText(text);
|
||||||
|
|
||||||
|
if (colors.Glow != default)
|
||||||
|
builder.Add(BuildColorEndPayload(_colorTypeGlow));
|
||||||
|
if (colors.Foreground != default)
|
||||||
|
builder.Add(BuildColorEndPayload(_colorTypeForeground));
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Colored SeString
|
#region Colored SeString
|
||||||
private const byte _colorTypeForeground = 0x13;
|
private const byte _colorTypeForeground = 0x13;
|
||||||
private const byte _colorTypeGlow = 0x14;
|
private const byte _colorTypeGlow = 0x14;
|
||||||
|
|
||||||
|
private static Colors SwapColorChannels(Colors colors)
|
||||||
|
=> new(SwapColorComponent(colors.Foreground), SwapColorComponent(colors.Glow));
|
||||||
|
|
||||||
|
private static uint SwapColorComponent(uint color)
|
||||||
|
{
|
||||||
|
if (color == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return ((color & 0xFFu) << 16) | (color & 0xFF00u) | ((color >> 16) & 0xFFu);
|
||||||
|
}
|
||||||
|
|
||||||
private static SeString BuildColoredSeString(string text, Colors colors)
|
private static SeString BuildColoredSeString(string text, Colors colors)
|
||||||
{
|
{
|
||||||
var ssb = new SeStringBuilder();
|
var ssb = new SeStringBuilder();
|
||||||
if (colors.Foreground != default)
|
AppendColoredSegment(ssb, text, colors);
|
||||||
ssb.Add(BuildColorStartPayload(_colorTypeForeground, colors.Foreground));
|
|
||||||
if (colors.Glow != default)
|
|
||||||
ssb.Add(BuildColorStartPayload(_colorTypeGlow, colors.Glow));
|
|
||||||
ssb.AddText(text);
|
|
||||||
if (colors.Glow != default)
|
|
||||||
ssb.Add(BuildColorEndPayload(_colorTypeGlow));
|
|
||||||
if (colors.Foreground != default)
|
|
||||||
ssb.Add(BuildColorEndPayload(_colorTypeForeground));
|
|
||||||
return ssb.Build();
|
return ssb.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static RawPayload BuildColorStartPayload(byte colorType, uint color)
|
private static RawPayload BuildColorStartPayload(byte colorType, uint color)
|
||||||
=> new(unchecked([0x02, colorType, 0x05, 0xF6, byte.Max((byte)color, 0x01), byte.Max((byte)(color >> 8), 0x01), byte.Max((byte)(color >> 16), 0x01), 0x03]));
|
=> new(unchecked([
|
||||||
|
0x02,
|
||||||
|
colorType,
|
||||||
|
0x05,
|
||||||
|
0xF6,
|
||||||
|
byte.Max((byte)color, (byte)0x01),
|
||||||
|
byte.Max((byte)(color >> 8), (byte)0x01),
|
||||||
|
byte.Max((byte)(color >> 16), (byte)0x01),
|
||||||
|
0x03
|
||||||
|
]));
|
||||||
|
|
||||||
private static RawPayload BuildColorEndPayload(byte colorType)
|
private static RawPayload BuildColorEndPayload(byte colorType)
|
||||||
=> new([0x02, colorType, 0x02, 0xEC, 0x03]);
|
=> new([0x02, colorType, 0x02, 0xEC, 0x03]);
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
Mediator.Subscribe<GposeStartMessage>(this, (_) => { _wasOpen = IsOpen; IsOpen = false; });
|
Mediator.Subscribe<GposeStartMessage>(this, (_) => { _wasOpen = IsOpen; IsOpen = false; });
|
||||||
Mediator.Subscribe<GposeEndMessage>(this, (_) => IsOpen = _wasOpen);
|
Mediator.Subscribe<GposeEndMessage>(this, (_) => IsOpen = _wasOpen);
|
||||||
Mediator.Subscribe<DisconnectedMessage>(this, (_) => IsOpen = false);
|
Mediator.Subscribe<DisconnectedMessage>(this, (_) => IsOpen = false);
|
||||||
Mediator.Subscribe<ClearProfileDataMessage>(this, (msg) =>
|
Mediator.Subscribe<ClearProfileUserDataMessage>(this, (msg) =>
|
||||||
{
|
{
|
||||||
if (msg.UserData == null || string.Equals(msg.UserData.UID, _apiController.UID, StringComparison.Ordinal))
|
if (msg.UserData == null || string.Equals(msg.UserData.UID, _apiController.UID, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
@@ -91,6 +91,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
protected override void DrawInternal()
|
protected override void DrawInternal()
|
||||||
{
|
{
|
||||||
|
|
||||||
_uiSharedService.UnderlinedBigText("Notes and Rules for Profiles", UIColors.Get("LightlessYellow"));
|
_uiSharedService.UnderlinedBigText("Notes and Rules for Profiles", UIColors.Get("LightlessYellow"));
|
||||||
ImGui.Dummy(new Vector2(5));
|
ImGui.Dummy(new Vector2(5));
|
||||||
|
|
||||||
@@ -108,7 +109,8 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
ImGui.Dummy(new Vector2(3));
|
ImGui.Dummy(new Vector2(3));
|
||||||
|
|
||||||
var profile = _lightlessProfileManager.GetLightlessProfile(new UserData(_apiController.UID));
|
var profile = _lightlessProfileManager.GetLightlessUserProfile(new UserData(_apiController.UID));
|
||||||
|
_logger.LogInformation("Profile fetched for drawing: {profile}", profile);
|
||||||
|
|
||||||
if (ImGui.BeginTabBar("##EditProfileTabs"))
|
if (ImGui.BeginTabBar("##EditProfileTabs"))
|
||||||
{
|
{
|
||||||
@@ -204,7 +206,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
_showFileDialogError = false;
|
_showFileDialogError = false;
|
||||||
await _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, Convert.ToBase64String(fileContent), Description: null))
|
await _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, Convert.ToBase64String(fileContent), Description: null, Tags: null))
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -213,7 +215,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear uploaded profile picture"))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear uploaded profile picture"))
|
||||||
{
|
{
|
||||||
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, "", Description: null));
|
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, "", Description: null, Tags: null));
|
||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("Clear your currently uploaded profile picture");
|
UiSharedService.AttachToolTip("Clear your currently uploaded profile picture");
|
||||||
if (_showFileDialogError)
|
if (_showFileDialogError)
|
||||||
@@ -223,7 +225,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
var isNsfw = profile.IsNSFW;
|
var isNsfw = profile.IsNSFW;
|
||||||
if (ImGui.Checkbox("Profile is NSFW", ref isNsfw))
|
if (ImGui.Checkbox("Profile is NSFW", ref isNsfw))
|
||||||
{
|
{
|
||||||
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, isNsfw, ProfilePictureBase64: null, Description: null));
|
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, isNsfw, ProfilePictureBase64: null, Description: null, Tags: null));
|
||||||
}
|
}
|
||||||
_uiSharedService.DrawHelpText("If your profile description or image can be considered NSFW, toggle this to ON");
|
_uiSharedService.DrawHelpText("If your profile description or image can be considered NSFW, toggle this to ON");
|
||||||
var widthTextBox = 400;
|
var widthTextBox = 400;
|
||||||
@@ -262,13 +264,13 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Description"))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Description"))
|
||||||
{
|
{
|
||||||
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, _descriptionText));
|
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, _descriptionText, Tags: null));
|
||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("Sets your profile description text");
|
UiSharedService.AttachToolTip("Sets your profile description text");
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear Description"))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear Description"))
|
||||||
{
|
{
|
||||||
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, ""));
|
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, "", Tags: null));
|
||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("Clears your profile description text");
|
UiSharedService.AttachToolTip("Clears your profile description text");
|
||||||
|
|
||||||
@@ -279,7 +281,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
_uiSharedService.MediumText("Supporter Vanity Settings", UIColors.Get("LightlessPurple"));
|
_uiSharedService.MediumText("Supporter Vanity Settings", UIColors.Get("LightlessPurple"));
|
||||||
ImGui.Dummy(new Vector2(4));
|
ImGui.Dummy(new Vector2(4));
|
||||||
_uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "Must be a supporter through Patreon/Ko-fi to access these settings.");
|
_uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "Must be a supporter through Patreon/Ko-fi to access these settings. If you have the vanity role, you must interact with the Discord bot first.");
|
||||||
|
|
||||||
var hasVanity = _apiController.HasVanity;
|
var hasVanity = _apiController.HasVanity;
|
||||||
|
|
||||||
|
|||||||
724
LightlessSync/UI/LightlessNotificationUI.cs
Normal file
724
LightlessSync/UI/LightlessNotificationUI.cs
Normal file
@@ -0,0 +1,724 @@
|
|||||||
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Interface.Colors;
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using Dalamud.Interface.Windowing;
|
||||||
|
using LightlessSync.LightlessConfiguration;
|
||||||
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
|
using LightlessSync.Services;
|
||||||
|
using LightlessSync.Services.Mediator;
|
||||||
|
using LightlessSync.UI.Models;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
using System.Numerics;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
|
||||||
|
namespace LightlessSync.UI;
|
||||||
|
|
||||||
|
public class LightlessNotificationUi : WindowMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private const float _notificationMinHeight = 60f;
|
||||||
|
private const float _notificationMaxHeight = 250f;
|
||||||
|
private const float _windowPaddingOffset = 6f;
|
||||||
|
private const float _slideAnimationDistance = 100f;
|
||||||
|
private const float _outAnimationSpeedMultiplier = 0.7f;
|
||||||
|
private const float _contentPaddingX = 10f;
|
||||||
|
private const float _contentPaddingY = 6f;
|
||||||
|
private const float _titleMessageSpacing = 4f;
|
||||||
|
private const float _actionButtonSpacing = 8f;
|
||||||
|
|
||||||
|
private readonly List<LightlessNotification> _notifications = new();
|
||||||
|
private readonly object _notificationLock = new();
|
||||||
|
private readonly LightlessConfigService _configService;
|
||||||
|
private readonly Dictionary<string, float> _notificationYOffsets = new();
|
||||||
|
private readonly Dictionary<string, float> _notificationTargetYOffsets = new();
|
||||||
|
|
||||||
|
public LightlessNotificationUi(ILogger<LightlessNotificationUi> logger, LightlessMediator mediator, PerformanceCollectorService performanceCollector, LightlessConfigService configService)
|
||||||
|
: base(logger, mediator, "Lightless Notifications##LightlessNotifications", performanceCollector)
|
||||||
|
{
|
||||||
|
_configService = configService;
|
||||||
|
Flags = ImGuiWindowFlags.NoDecoration |
|
||||||
|
ImGuiWindowFlags.NoMove |
|
||||||
|
ImGuiWindowFlags.NoResize |
|
||||||
|
ImGuiWindowFlags.NoSavedSettings |
|
||||||
|
ImGuiWindowFlags.NoFocusOnAppearing |
|
||||||
|
ImGuiWindowFlags.NoNav |
|
||||||
|
ImGuiWindowFlags.NoBackground |
|
||||||
|
ImGuiWindowFlags.NoCollapse |
|
||||||
|
ImGuiWindowFlags.AlwaysAutoResize;
|
||||||
|
|
||||||
|
PositionCondition = ImGuiCond.Always;
|
||||||
|
SizeCondition = ImGuiCond.FirstUseEver;
|
||||||
|
IsOpen = false;
|
||||||
|
RespectCloseHotkey = false;
|
||||||
|
DisableWindowSounds = true;
|
||||||
|
|
||||||
|
Mediator.Subscribe<LightlessNotificationMessage>(this, HandleNotificationMessage);
|
||||||
|
Mediator.Subscribe<LightlessNotificationDismissMessage>(this, HandleNotificationDismissMessage);
|
||||||
|
Mediator.Subscribe<ClearAllNotificationsMessage>(this, HandleClearAllNotifications);
|
||||||
|
}
|
||||||
|
private void HandleNotificationMessage(LightlessNotificationMessage message) => AddNotification(message.Notification);
|
||||||
|
private void HandleNotificationDismissMessage(LightlessNotificationDismissMessage message) => RemoveNotification(message.NotificationId);
|
||||||
|
private void HandleClearAllNotifications(ClearAllNotificationsMessage message) => ClearAllNotifications();
|
||||||
|
|
||||||
|
public void AddNotification(LightlessNotification notification)
|
||||||
|
{
|
||||||
|
lock (_notificationLock)
|
||||||
|
{
|
||||||
|
var existingNotification = _notifications.FirstOrDefault(n => n.Id == notification.Id);
|
||||||
|
if (existingNotification != null)
|
||||||
|
{
|
||||||
|
UpdateExistingNotification(existingNotification, notification);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_notifications.Add(notification);
|
||||||
|
_logger.LogDebug("Added new notification: {Title}", notification.Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsOpen) IsOpen = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateExistingNotification(LightlessNotification existing, LightlessNotification updated)
|
||||||
|
{
|
||||||
|
existing.Message = updated.Message;
|
||||||
|
existing.Progress = updated.Progress;
|
||||||
|
existing.ShowProgress = updated.ShowProgress;
|
||||||
|
existing.Title = updated.Title;
|
||||||
|
_logger.LogDebug("Updated existing notification: {Title}", updated.Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveNotification(string id)
|
||||||
|
{
|
||||||
|
lock (_notificationLock)
|
||||||
|
{
|
||||||
|
var notification = _notifications.FirstOrDefault(n => n.Id == id);
|
||||||
|
if (notification != null)
|
||||||
|
{
|
||||||
|
StartOutAnimation(notification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearAllNotifications()
|
||||||
|
{
|
||||||
|
lock (_notificationLock)
|
||||||
|
{
|
||||||
|
foreach (var notification in _notifications)
|
||||||
|
{
|
||||||
|
StartOutAnimation(notification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartOutAnimation(LightlessNotification notification)
|
||||||
|
{
|
||||||
|
notification.IsAnimatingOut = true;
|
||||||
|
notification.IsAnimatingIn = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ShouldRemoveNotification(LightlessNotification notification) =>
|
||||||
|
notification.IsAnimatingOut && notification.AnimationProgress <= 0.01f;
|
||||||
|
|
||||||
|
protected override void DrawInternal()
|
||||||
|
{
|
||||||
|
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero);
|
||||||
|
|
||||||
|
lock (_notificationLock)
|
||||||
|
{
|
||||||
|
UpdateNotifications();
|
||||||
|
|
||||||
|
if (_notifications.Count == 0)
|
||||||
|
{
|
||||||
|
ImGui.PopStyleVar();
|
||||||
|
IsOpen = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var viewport = ImGui.GetMainViewport();
|
||||||
|
|
||||||
|
// Window auto-resizes based on content (AlwaysAutoResize flag)
|
||||||
|
Position = CalculateWindowPosition(viewport);
|
||||||
|
PositionCondition = ImGuiCond.Always;
|
||||||
|
|
||||||
|
DrawAllNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.PopStyleVar();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2 CalculateWindowPosition(ImGuiViewportPtr viewport)
|
||||||
|
{
|
||||||
|
var corner = _configService.Current.NotificationCorner;
|
||||||
|
var offsetX = _configService.Current.NotificationOffsetX;
|
||||||
|
var width = _configService.Current.NotificationWidth;
|
||||||
|
|
||||||
|
float posX = corner == NotificationCorner.Left
|
||||||
|
? viewport.WorkPos.X + offsetX - _windowPaddingOffset
|
||||||
|
: viewport.WorkPos.X + viewport.WorkSize.X - width - offsetX - _windowPaddingOffset;
|
||||||
|
|
||||||
|
return new Vector2(posX, viewport.WorkPos.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawAllNotifications()
|
||||||
|
{
|
||||||
|
var offsetY = _configService.Current.NotificationOffsetY;
|
||||||
|
var startY = ImGui.GetCursorPosY() + offsetY;
|
||||||
|
|
||||||
|
for (int i = 0; i < _notifications.Count; i++)
|
||||||
|
{
|
||||||
|
var notification = _notifications[i];
|
||||||
|
|
||||||
|
if (_notificationYOffsets.TryGetValue(notification.Id, out var yOffset))
|
||||||
|
{
|
||||||
|
ImGui.SetCursorPosY(startY + yOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawNotification(notification, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateNotifications()
|
||||||
|
{
|
||||||
|
var deltaTime = ImGui.GetIO().DeltaTime;
|
||||||
|
EnforceMaxNotificationLimit();
|
||||||
|
UpdateAnimationsAndRemoveExpired(deltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnforceMaxNotificationLimit()
|
||||||
|
{
|
||||||
|
var maxNotifications = _configService.Current.MaxSimultaneousNotifications;
|
||||||
|
while (_notifications.Count(n => !n.IsAnimatingOut) > maxNotifications)
|
||||||
|
{
|
||||||
|
var oldestNotification = _notifications
|
||||||
|
.Where(n => !n.IsAnimatingOut)
|
||||||
|
.OrderBy(n => n.CreatedAt)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
if (oldestNotification != null)
|
||||||
|
{
|
||||||
|
StartOutAnimation(oldestNotification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateAnimationsAndRemoveExpired(float deltaTime)
|
||||||
|
{
|
||||||
|
UpdateTargetYPositions();
|
||||||
|
|
||||||
|
for (int i = _notifications.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var notification = _notifications[i];
|
||||||
|
UpdateNotificationAnimation(notification, deltaTime);
|
||||||
|
UpdateNotificationYOffset(notification, deltaTime);
|
||||||
|
|
||||||
|
if (ShouldRemoveNotification(notification))
|
||||||
|
{
|
||||||
|
_notifications.RemoveAt(i);
|
||||||
|
_notificationYOffsets.Remove(notification.Id);
|
||||||
|
_notificationTargetYOffsets.Remove(notification.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateTargetYPositions()
|
||||||
|
{
|
||||||
|
float currentY = 0f;
|
||||||
|
|
||||||
|
for (int i = 0; i < _notifications.Count; i++)
|
||||||
|
{
|
||||||
|
var notification = _notifications[i];
|
||||||
|
|
||||||
|
if (!_notificationTargetYOffsets.ContainsKey(notification.Id))
|
||||||
|
{
|
||||||
|
_notificationTargetYOffsets[notification.Id] = currentY;
|
||||||
|
_notificationYOffsets[notification.Id] = currentY;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_notificationTargetYOffsets[notification.Id] = currentY;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentY += CalculateNotificationHeight(notification) + _configService.Current.NotificationSpacing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateNotificationYOffset(LightlessNotification notification, float deltaTime)
|
||||||
|
{
|
||||||
|
if (!_notificationYOffsets.ContainsKey(notification.Id) || !_notificationTargetYOffsets.ContainsKey(notification.Id))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var current = _notificationYOffsets[notification.Id];
|
||||||
|
var target = _notificationTargetYOffsets[notification.Id];
|
||||||
|
var diff = target - current;
|
||||||
|
|
||||||
|
if (Math.Abs(diff) < 0.5f)
|
||||||
|
{
|
||||||
|
_notificationYOffsets[notification.Id] = target;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var speed = _configService.Current.NotificationSlideSpeed;
|
||||||
|
_notificationYOffsets[notification.Id] = current + (diff * deltaTime * speed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateNotificationAnimation(LightlessNotification notification, float deltaTime)
|
||||||
|
{
|
||||||
|
if (notification.IsAnimatingIn && notification.AnimationProgress < 1f)
|
||||||
|
{
|
||||||
|
notification.AnimationProgress = Math.Min(1f,
|
||||||
|
notification.AnimationProgress + deltaTime * _configService.Current.NotificationAnimationSpeed);
|
||||||
|
}
|
||||||
|
else if (notification.IsAnimatingOut && notification.AnimationProgress > 0f)
|
||||||
|
{
|
||||||
|
notification.AnimationProgress = Math.Max(0f,
|
||||||
|
notification.AnimationProgress - deltaTime * _configService.Current.NotificationAnimationSpeed * _outAnimationSpeedMultiplier);
|
||||||
|
}
|
||||||
|
else if (!notification.IsAnimatingOut && !notification.IsDismissed)
|
||||||
|
{
|
||||||
|
notification.IsAnimatingIn = false;
|
||||||
|
|
||||||
|
if (notification.IsExpired)
|
||||||
|
{
|
||||||
|
StartOutAnimation(notification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2 CalculateSlideOffset(float alpha)
|
||||||
|
{
|
||||||
|
var distance = (1f - alpha) * _slideAnimationDistance;
|
||||||
|
var corner = _configService.Current.NotificationCorner;
|
||||||
|
return corner == NotificationCorner.Left ? new Vector2(-distance, 0) : new Vector2(distance, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawNotification(LightlessNotification notification, int index)
|
||||||
|
{
|
||||||
|
var alpha = notification.AnimationProgress;
|
||||||
|
if (alpha <= 0f) return;
|
||||||
|
|
||||||
|
var slideOffset = CalculateSlideOffset(alpha);
|
||||||
|
var originalCursorPos = ImGui.GetCursorPos();
|
||||||
|
ImGui.SetCursorPos(originalCursorPos + slideOffset);
|
||||||
|
|
||||||
|
var notificationHeight = CalculateNotificationHeight(notification);
|
||||||
|
var notificationWidth = _configService.Current.NotificationWidth;
|
||||||
|
|
||||||
|
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero);
|
||||||
|
|
||||||
|
using var child = ImRaii.Child($"notification_{notification.Id}",
|
||||||
|
new Vector2(notificationWidth, notificationHeight),
|
||||||
|
false, ImGuiWindowFlags.NoScrollbar);
|
||||||
|
|
||||||
|
if (child.Success)
|
||||||
|
{
|
||||||
|
DrawNotificationContent(notification, alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.PopStyleVar();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawNotificationContent(LightlessNotification notification, float alpha)
|
||||||
|
{
|
||||||
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
|
var windowPos = ImGui.GetWindowPos();
|
||||||
|
var windowSize = ImGui.GetWindowSize();
|
||||||
|
|
||||||
|
var bgColor = CalculateBackgroundColor(alpha, ImGui.IsWindowHovered());
|
||||||
|
var accentColor = GetNotificationAccentColor(notification.Type);
|
||||||
|
accentColor.W *= alpha;
|
||||||
|
|
||||||
|
DrawShadow(drawList, windowPos, windowSize, alpha);
|
||||||
|
HandleClickToDismiss(notification);
|
||||||
|
DrawBackground(drawList, windowPos, windowSize, bgColor);
|
||||||
|
DrawAccentBar(drawList, windowPos, windowSize, accentColor);
|
||||||
|
DrawDurationProgressBar(notification, alpha, windowPos, windowSize, drawList);
|
||||||
|
DrawNotificationText(notification, alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector4 CalculateBackgroundColor(float alpha, bool isHovered)
|
||||||
|
{
|
||||||
|
var baseOpacity = _configService.Current.NotificationOpacity;
|
||||||
|
var finalOpacity = baseOpacity * alpha;
|
||||||
|
var bgColor = new Vector4(30f/255f, 30f/255f, 30f/255f, finalOpacity);
|
||||||
|
|
||||||
|
if (isHovered)
|
||||||
|
{
|
||||||
|
bgColor *= 1.1f;
|
||||||
|
bgColor.W = Math.Min(bgColor.W, 0.98f);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bgColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawShadow(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float alpha)
|
||||||
|
{
|
||||||
|
var shadowOffset = new Vector2(1f, 1f);
|
||||||
|
var shadowColor = new Vector4(0f, 0f, 0f, 0.4f * alpha);
|
||||||
|
drawList.AddRectFilled(
|
||||||
|
windowPos + shadowOffset,
|
||||||
|
windowPos + windowSize + shadowOffset,
|
||||||
|
ImGui.ColorConvertFloat4ToU32(shadowColor),
|
||||||
|
3f
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleClickToDismiss(LightlessNotification notification)
|
||||||
|
{
|
||||||
|
if (ImGui.IsWindowHovered() &&
|
||||||
|
_configService.Current.DismissNotificationOnClick &&
|
||||||
|
!notification.Actions.Any() &&
|
||||||
|
ImGui.IsMouseClicked(ImGuiMouseButton.Left))
|
||||||
|
{
|
||||||
|
notification.IsDismissed = true;
|
||||||
|
StartOutAnimation(notification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, Vector4 bgColor)
|
||||||
|
{
|
||||||
|
drawList.AddRectFilled(
|
||||||
|
windowPos,
|
||||||
|
windowPos + windowSize,
|
||||||
|
ImGui.ColorConvertFloat4ToU32(bgColor),
|
||||||
|
3f
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawAccentBar(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, Vector4 accentColor)
|
||||||
|
{
|
||||||
|
var accentWidth = _configService.Current.NotificationAccentBarWidth;
|
||||||
|
if (accentWidth <= 0f) return;
|
||||||
|
|
||||||
|
var corner = _configService.Current.NotificationCorner;
|
||||||
|
Vector2 accentStart, accentEnd;
|
||||||
|
|
||||||
|
if (corner == NotificationCorner.Left)
|
||||||
|
{
|
||||||
|
accentStart = windowPos + new Vector2(windowSize.X - accentWidth, 0);
|
||||||
|
accentEnd = windowPos + windowSize;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
accentStart = windowPos;
|
||||||
|
accentEnd = windowPos + new Vector2(accentWidth, windowSize.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawList.AddRectFilled(
|
||||||
|
accentStart,
|
||||||
|
accentEnd,
|
||||||
|
ImGui.ColorConvertFloat4ToU32(accentColor),
|
||||||
|
3f
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawDurationProgressBar(LightlessNotification notification, float alpha, Vector2 windowPos, Vector2 windowSize, ImDrawListPtr drawList)
|
||||||
|
{
|
||||||
|
var progress = CalculateProgress(notification);
|
||||||
|
var progressBarColor = UIColors.Get("LightlessBlue");
|
||||||
|
var progressHeight = 2f;
|
||||||
|
var progressY = windowPos.Y + windowSize.Y - progressHeight;
|
||||||
|
var progressWidth = windowSize.X * progress;
|
||||||
|
|
||||||
|
DrawProgressBackground(drawList, windowPos, windowSize, progressY, progressHeight, progressBarColor, alpha);
|
||||||
|
|
||||||
|
if (progress > 0)
|
||||||
|
{
|
||||||
|
DrawProgressForeground(drawList, windowPos, progressY, progressHeight, progressWidth, progressBarColor, alpha);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float CalculateProgress(LightlessNotification notification)
|
||||||
|
{
|
||||||
|
if (notification.Type == NotificationType.Download && notification.ShowProgress)
|
||||||
|
{
|
||||||
|
return Math.Clamp(notification.Progress, 0f, 1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
var elapsed = DateTime.UtcNow - notification.CreatedAt;
|
||||||
|
return Math.Min(1.0f, (float)(elapsed.TotalSeconds / notification.Duration.TotalSeconds));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawProgressBackground(ImDrawListPtr drawList, Vector2 windowPos, Vector2 windowSize, float progressY, float progressHeight, Vector4 progressBarColor, float alpha)
|
||||||
|
{
|
||||||
|
var bgProgressColor = new Vector4(progressBarColor.X * 0.3f, progressBarColor.Y * 0.3f, progressBarColor.Z * 0.3f, 0.5f * alpha);
|
||||||
|
drawList.AddRectFilled(
|
||||||
|
new Vector2(windowPos.X, progressY),
|
||||||
|
new Vector2(windowPos.X + windowSize.X, progressY + progressHeight),
|
||||||
|
ImGui.ColorConvertFloat4ToU32(bgProgressColor),
|
||||||
|
0f
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawProgressForeground(ImDrawListPtr drawList, Vector2 windowPos, float progressY, float progressHeight, float progressWidth, Vector4 progressBarColor, float alpha)
|
||||||
|
{
|
||||||
|
var progressColor = progressBarColor;
|
||||||
|
progressColor.W *= alpha;
|
||||||
|
drawList.AddRectFilled(
|
||||||
|
new Vector2(windowPos.X, progressY),
|
||||||
|
new Vector2(windowPos.X + progressWidth, progressY + progressHeight),
|
||||||
|
ImGui.ColorConvertFloat4ToU32(progressColor),
|
||||||
|
0f
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawNotificationText(LightlessNotification notification, float alpha)
|
||||||
|
{
|
||||||
|
var contentPos = new Vector2(_contentPaddingX, _contentPaddingY);
|
||||||
|
var windowSize = ImGui.GetWindowSize();
|
||||||
|
var contentWidth = CalculateContentWidth(windowSize.X);
|
||||||
|
|
||||||
|
ImGui.SetCursorPos(contentPos);
|
||||||
|
|
||||||
|
var titleHeight = DrawTitle(notification, contentWidth, alpha);
|
||||||
|
DrawMessage(notification, contentPos, contentWidth, titleHeight, alpha);
|
||||||
|
|
||||||
|
if (HasActions(notification))
|
||||||
|
{
|
||||||
|
PositionActionsAtBottom(windowSize.Y);
|
||||||
|
DrawNotificationActions(notification, contentWidth, alpha);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float CalculateContentWidth(float windowWidth) =>
|
||||||
|
windowWidth - (_contentPaddingX * 2);
|
||||||
|
|
||||||
|
private bool HasActions(LightlessNotification notification) =>
|
||||||
|
notification.Actions.Count > 0;
|
||||||
|
|
||||||
|
private void PositionActionsAtBottom(float windowHeight)
|
||||||
|
{
|
||||||
|
var actionHeight = ImGui.GetFrameHeight();
|
||||||
|
var bottomY = windowHeight - _contentPaddingY - actionHeight;
|
||||||
|
ImGui.SetCursorPosY(bottomY);
|
||||||
|
ImGui.SetCursorPosX(_contentPaddingX);
|
||||||
|
}
|
||||||
|
|
||||||
|
private float DrawTitle(LightlessNotification notification, float contentWidth, float alpha)
|
||||||
|
{
|
||||||
|
var titleColor = new Vector4(1f, 1f, 1f, alpha);
|
||||||
|
var titleText = FormatTitleText(notification);
|
||||||
|
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, titleColor))
|
||||||
|
{
|
||||||
|
return DrawWrappedText(titleText, contentWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FormatTitleText(LightlessNotification notification)
|
||||||
|
{
|
||||||
|
if (!_configService.Current.ShowNotificationTimestamp)
|
||||||
|
return notification.Title;
|
||||||
|
|
||||||
|
var timestamp = notification.CreatedAt.ToLocalTime().ToString("HH:mm:ss");
|
||||||
|
return $"[{timestamp}] {notification.Title}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private float DrawWrappedText(string text, float wrapWidth)
|
||||||
|
{
|
||||||
|
ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + wrapWidth);
|
||||||
|
var startY = ImGui.GetCursorPosY();
|
||||||
|
ImGui.TextWrapped(text);
|
||||||
|
var height = ImGui.GetCursorPosY() - startY;
|
||||||
|
ImGui.PopTextWrapPos();
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawMessage(LightlessNotification notification, Vector2 contentPos, float contentWidth, float titleHeight, float alpha)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(notification.Message)) return;
|
||||||
|
|
||||||
|
var messagePos = contentPos + new Vector2(0f, titleHeight + _titleMessageSpacing);
|
||||||
|
var messageColor = new Vector4(0.9f, 0.9f, 0.9f, alpha);
|
||||||
|
|
||||||
|
ImGui.SetCursorPos(messagePos);
|
||||||
|
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, messageColor))
|
||||||
|
{
|
||||||
|
DrawWrappedText(notification.Message, contentWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawNotificationActions(LightlessNotification notification, float availableWidth, float alpha)
|
||||||
|
{
|
||||||
|
var buttonWidth = CalculateActionButtonWidth(notification.Actions.Count, availableWidth);
|
||||||
|
|
||||||
|
_logger.LogDebug("Drawing {ActionCount} notification actions, buttonWidth: {ButtonWidth}, availableWidth: {AvailableWidth}",
|
||||||
|
notification.Actions.Count, buttonWidth, availableWidth);
|
||||||
|
|
||||||
|
var startX = ImGui.GetCursorPosX();
|
||||||
|
|
||||||
|
for (int i = 0; i < notification.Actions.Count; i++)
|
||||||
|
{
|
||||||
|
if (i > 0)
|
||||||
|
{
|
||||||
|
ImGui.SameLine();
|
||||||
|
PositionActionButton(i, startX, buttonWidth);
|
||||||
|
}
|
||||||
|
DrawActionButton(notification.Actions[i], notification, alpha, buttonWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float CalculateActionButtonWidth(int actionCount, float availableWidth)
|
||||||
|
{
|
||||||
|
var totalSpacing = (actionCount - 1) * _actionButtonSpacing;
|
||||||
|
return (availableWidth - totalSpacing) / actionCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PositionActionButton(int index, float startX, float buttonWidth)
|
||||||
|
{
|
||||||
|
var xPosition = startX + index * (buttonWidth + _actionButtonSpacing);
|
||||||
|
ImGui.SetCursorPosX(xPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawActionButton(LightlessNotificationAction action, LightlessNotification notification, float alpha, float buttonWidth)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Drawing action button: {ActionId} - {ActionLabel}, width: {ButtonWidth}", action.Id, action.Label, buttonWidth);
|
||||||
|
|
||||||
|
var buttonColor = action.Color;
|
||||||
|
buttonColor.W *= alpha;
|
||||||
|
|
||||||
|
var hoveredColor = buttonColor * 1.1f;
|
||||||
|
hoveredColor.W = buttonColor.W;
|
||||||
|
|
||||||
|
var activeColor = buttonColor * 0.9f;
|
||||||
|
activeColor.W = buttonColor.W;
|
||||||
|
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Button, buttonColor))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, hoveredColor))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.ButtonActive, activeColor))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(1f, 1f, 1f, alpha)))
|
||||||
|
{
|
||||||
|
var buttonPressed = false;
|
||||||
|
|
||||||
|
if (action.Icon != FontAwesomeIcon.None)
|
||||||
|
{
|
||||||
|
buttonPressed = DrawIconTextButton(action.Icon, action.Label, buttonWidth, alpha);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
buttonPressed = ImGui.Button(action.Label, new Vector2(buttonWidth, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Button {ActionId} pressed: {ButtonPressed}", action.Id, buttonPressed);
|
||||||
|
|
||||||
|
if (buttonPressed)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Executing action: {ActionId}", action.Id);
|
||||||
|
action.OnClick(notification);
|
||||||
|
_logger.LogDebug("Action executed successfully: {ActionId}", action.Id);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error executing notification action: {ActionId}", action.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool DrawIconTextButton(FontAwesomeIcon icon, string text, float width, float alpha)
|
||||||
|
{
|
||||||
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
|
var cursorPos = ImGui.GetCursorScreenPos();
|
||||||
|
var frameHeight = ImGui.GetFrameHeight();
|
||||||
|
|
||||||
|
Vector2 iconSize;
|
||||||
|
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||||
|
{
|
||||||
|
iconSize = ImGui.CalcTextSize(icon.ToIconString());
|
||||||
|
}
|
||||||
|
|
||||||
|
var textSize = ImGui.CalcTextSize(text);
|
||||||
|
var spacing = 3f * ImGuiHelpers.GlobalScale;
|
||||||
|
var totalTextWidth = iconSize.X + spacing + textSize.X;
|
||||||
|
|
||||||
|
var buttonPressed = ImGui.InvisibleButton($"btn_{icon}_{text}", new Vector2(width, frameHeight));
|
||||||
|
|
||||||
|
var buttonMin = ImGui.GetItemRectMin();
|
||||||
|
var buttonMax = ImGui.GetItemRectMax();
|
||||||
|
var buttonSize = buttonMax - buttonMin;
|
||||||
|
|
||||||
|
var buttonColor = ImGui.GetColorU32(ImGuiCol.Button);
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
buttonColor = ImGui.GetColorU32(ImGuiCol.ButtonHovered);
|
||||||
|
if (ImGui.IsItemActive())
|
||||||
|
buttonColor = ImGui.GetColorU32(ImGuiCol.ButtonActive);
|
||||||
|
|
||||||
|
drawList.AddRectFilled(buttonMin, buttonMax, buttonColor, 3f);
|
||||||
|
|
||||||
|
var iconPos = buttonMin + new Vector2((buttonSize.X - totalTextWidth) / 2f, (buttonSize.Y - iconSize.Y) / 2f);
|
||||||
|
var textPos = iconPos + new Vector2(iconSize.X + spacing, (iconSize.Y - textSize.Y) / 2f);
|
||||||
|
|
||||||
|
var textColor = ImGui.GetColorU32(ImGuiCol.Text);
|
||||||
|
|
||||||
|
// Draw icon
|
||||||
|
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||||
|
{
|
||||||
|
drawList.AddText(iconPos, textColor, icon.ToIconString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw text
|
||||||
|
drawList.AddText(textPos, textColor, text);
|
||||||
|
|
||||||
|
return buttonPressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float CalculateNotificationHeight(LightlessNotification notification)
|
||||||
|
{
|
||||||
|
var contentWidth = CalculateContentWidth(_configService.Current.NotificationWidth);
|
||||||
|
var height = 12f;
|
||||||
|
|
||||||
|
height += CalculateTitleHeight(notification, contentWidth);
|
||||||
|
height += CalculateMessageHeight(notification, contentWidth);
|
||||||
|
|
||||||
|
if (notification.ShowProgress)
|
||||||
|
{
|
||||||
|
height += 12f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notification.Actions.Count > 0)
|
||||||
|
{
|
||||||
|
height += ImGui.GetStyle().ItemSpacing.Y;
|
||||||
|
height += ImGui.GetFrameHeight();
|
||||||
|
height += 12f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.Clamp(height, _notificationMinHeight, _notificationMaxHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private float CalculateTitleHeight(LightlessNotification notification, float contentWidth)
|
||||||
|
{
|
||||||
|
var titleText = _configService.Current.ShowNotificationTimestamp
|
||||||
|
? $"[{notification.CreatedAt.ToLocalTime():HH:mm:ss}] {notification.Title}"
|
||||||
|
: notification.Title;
|
||||||
|
|
||||||
|
return ImGui.CalcTextSize(titleText, true, contentWidth).Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float CalculateMessageHeight(LightlessNotification notification, float contentWidth)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(notification.Message)) return 0f;
|
||||||
|
|
||||||
|
var messageHeight = ImGui.CalcTextSize(notification.Message, true, contentWidth).Y;
|
||||||
|
return 4f + messageHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector4 GetNotificationAccentColor(NotificationType type)
|
||||||
|
{
|
||||||
|
return type switch
|
||||||
|
{
|
||||||
|
NotificationType.Info => UIColors.Get("LightlessPurple"),
|
||||||
|
NotificationType.Warning => UIColors.Get("LightlessYellow"),
|
||||||
|
NotificationType.Error => UIColors.Get("DimRed"),
|
||||||
|
NotificationType.PairRequest => UIColors.Get("LightlessBlue"),
|
||||||
|
NotificationType.Download => UIColors.Get("LightlessGreen"),
|
||||||
|
NotificationType.Performance => UIColors.Get("LightlessOrange"),
|
||||||
|
|
||||||
|
_ => UIColors.Get("LightlessPurple")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
43
LightlessSync/UI/Models/Changelog.cs
Normal file
43
LightlessSync/UI/Models/Changelog.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
namespace LightlessSync.UI.Models
|
||||||
|
{
|
||||||
|
public class ChangelogFile
|
||||||
|
{
|
||||||
|
public string Tagline { get; init; } = string.Empty;
|
||||||
|
public string Subline { get; init; } = string.Empty;
|
||||||
|
public List<ChangelogEntry> Changelog { get; init; } = new();
|
||||||
|
public List<CreditCategory>? Credits { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ChangelogEntry
|
||||||
|
{
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
public string Date { get; init; } = string.Empty;
|
||||||
|
public string Tagline { get; init; } = string.Empty;
|
||||||
|
public bool? IsCurrent { get; init; }
|
||||||
|
public string? Message { get; init; }
|
||||||
|
public List<ChangelogVersion>? Versions { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ChangelogVersion
|
||||||
|
{
|
||||||
|
public string Number { get; init; } = string.Empty;
|
||||||
|
public List<string> Items { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CreditCategory
|
||||||
|
{
|
||||||
|
public string Category { get; init; } = string.Empty;
|
||||||
|
public List<CreditItem> Items { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CreditItem
|
||||||
|
{
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
public string Role { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CreditsFile
|
||||||
|
{
|
||||||
|
public List<CreditCategory> Credits { get; init; } = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
32
LightlessSync/UI/Models/LightlessNotification.cs
Normal file
32
LightlessSync/UI/Models/LightlessNotification.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using Dalamud.Interface;
|
||||||
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
|
using System.Numerics;
|
||||||
|
namespace LightlessSync.UI.Models;
|
||||||
|
public class LightlessNotification
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public NotificationType Type { get; set; } = NotificationType.Info;
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public TimeSpan Duration { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
|
public bool IsExpired => DateTime.UtcNow - CreatedAt > Duration;
|
||||||
|
public bool IsDismissed { get; set; } = false;
|
||||||
|
public List<LightlessNotificationAction> Actions { get; set; } = new();
|
||||||
|
public bool ShowProgress { get; set; } = false;
|
||||||
|
public float Progress { get; set; } = 0f;
|
||||||
|
public float AnimationProgress { get; set; } = 0f;
|
||||||
|
public bool IsAnimatingIn { get; set; } = true;
|
||||||
|
public bool IsAnimatingOut { get; set; } = false;
|
||||||
|
public uint? SoundEffectId { get; set; } = null;
|
||||||
|
}
|
||||||
|
public class LightlessNotificationAction
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||||
|
public string Label { get; set; } = string.Empty;
|
||||||
|
public FontAwesomeIcon Icon { get; set; } = FontAwesomeIcon.None;
|
||||||
|
public Vector4 Color { get; set; } = Vector4.One;
|
||||||
|
public Action<LightlessNotification> OnClick { get; set; } = _ => { };
|
||||||
|
public bool IsPrimary { get; set; } = false;
|
||||||
|
public bool IsDestructive { get; set; } = false;
|
||||||
|
}
|
||||||
72
LightlessSync/UI/Models/NotificationSounds.cs
Normal file
72
LightlessSync/UI/Models/NotificationSounds.cs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
|
|
||||||
|
namespace LightlessSync.UI.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Common FFXIV <se.#> sound effect IDs for notifications.
|
||||||
|
/// These correspond to the same sound IDs used in macros (1–16).
|
||||||
|
/// </summary>
|
||||||
|
public static class NotificationSounds
|
||||||
|
{
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Base <se.#> IDs (1–16)
|
||||||
|
// https://ffxiv.consolegameswiki.com/wiki/Macros#Sound_Effects
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
public const uint Se1 = 1; // Soft chime
|
||||||
|
public const uint Se2 = 2; // Higher chime
|
||||||
|
public const uint Se3 = 3; // Bell tone
|
||||||
|
public const uint Se4 = 4; // Harp tone
|
||||||
|
public const uint Se5 = 5; // Mechanical click
|
||||||
|
public const uint Se6 = 6; // Drum / percussion
|
||||||
|
public const uint Se7 = 7; // Metallic chime
|
||||||
|
public const uint Se8 = 8; // Wooden tone
|
||||||
|
public const uint Se9 = 9; // Wind / flute tone
|
||||||
|
public const uint Se10 = 11; // Magical sparkle (ID 10 is skipped in game)
|
||||||
|
public const uint Se11 = 12; // Metallic ring
|
||||||
|
public const uint Se12 = 13; // Deep thud
|
||||||
|
public const uint Se13 = 14; // "Tell received" ping
|
||||||
|
public const uint Se14 = 15; // Success fanfare
|
||||||
|
public const uint Se15 = 16; // System warning
|
||||||
|
// Note: Se16 doesn't exist - Se15 is the last available sound
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// General notification sound (<se.2>)
|
||||||
|
/// </summary>
|
||||||
|
public const uint Info = Se2;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Warning/alert sound (<se.15>)
|
||||||
|
/// </summary>
|
||||||
|
public const uint Warning = Se15;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Error sound (<se.15> - System warning, used for errors)
|
||||||
|
/// </summary>
|
||||||
|
public const uint Error = Se15;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Success sound (<se.14>)
|
||||||
|
/// </summary>
|
||||||
|
public const uint Success = Se14;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pair request sound (<se.13>, same as tell notification)
|
||||||
|
/// </summary>
|
||||||
|
public const uint PairRequest = Se13;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Download complete sound (<se.10>, a clean sparkle tone)
|
||||||
|
/// </summary>
|
||||||
|
public const uint DownloadComplete = Se10;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get default sound for notification type
|
||||||
|
/// </summary>
|
||||||
|
public static uint GetDefaultSound(NotificationType type) => type switch
|
||||||
|
{
|
||||||
|
NotificationType.Info => Info,
|
||||||
|
NotificationType.Warning => Warning,
|
||||||
|
NotificationType.Error => Error,
|
||||||
|
_ => Info
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -85,7 +85,7 @@ public class PopoutProfileUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
var spacing = ImGui.GetStyle().ItemSpacing;
|
var spacing = ImGui.GetStyle().ItemSpacing;
|
||||||
|
|
||||||
var lightlessProfile = _lightlessProfileManager.GetLightlessProfile(_pair.UserData);
|
var lightlessProfile = _lightlessProfileManager.GetLightlessUserProfile(_pair.UserData);
|
||||||
|
|
||||||
if (_textureWrap == null || !lightlessProfile.ImageData.Value.SequenceEqual(_lastProfilePicture))
|
if (_textureWrap == null || !lightlessProfile.ImageData.Value.SequenceEqual(_lastProfilePicture))
|
||||||
{
|
{
|
||||||
|
|||||||
12
LightlessSync/UI/ProfileTags.cs
Normal file
12
LightlessSync/UI/ProfileTags.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace LightlessSync.UI
|
||||||
|
{
|
||||||
|
public enum ProfileTags
|
||||||
|
{
|
||||||
|
SFW = 0,
|
||||||
|
NSFW = 1,
|
||||||
|
RP = 2,
|
||||||
|
ERP = 3,
|
||||||
|
Venues = 4,
|
||||||
|
Gpose = 5
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -51,7 +51,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
var spacing = ImGui.GetStyle().ItemSpacing;
|
var spacing = ImGui.GetStyle().ItemSpacing;
|
||||||
|
|
||||||
var lightlessProfile = _lightlessProfileManager.GetLightlessProfile(Pair.UserData);
|
var lightlessProfile = _lightlessProfileManager.GetLightlessUserProfile(Pair.UserData);
|
||||||
|
|
||||||
if (_textureWrap == null || !lightlessProfile.ImageData.Value.SequenceEqual(_lastProfilePicture))
|
if (_textureWrap == null || !lightlessProfile.ImageData.Value.SequenceEqual(_lastProfilePicture))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,169 +1,231 @@
|
|||||||
// inspiration: brio because it's style is fucking amazing
|
// inspiration: brio because it's style is fucking amazing
|
||||||
|
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
|
using LightlessSync.LightlessConfiguration.Configurations;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
|
||||||
namespace LightlessSync.UI.Style
|
namespace LightlessSync.UI.Style;
|
||||||
|
|
||||||
|
internal static class MainStyle
|
||||||
{
|
{
|
||||||
internal static class MainStyle
|
public readonly record struct StyleColorOption(string Key, string Label, Func<Vector4> DefaultValue, ImGuiCol Target, string? Description = null, string? UiColorKey = null);
|
||||||
|
public readonly record struct StyleFloatOption(string Key, string Label, float DefaultValue, ImGuiStyleVar Target, float? Min = null, float? Max = null, float Speed = 0.25f, string? Description = null);
|
||||||
|
public readonly record struct StyleVector2Option(string Key, string Label, Func<Vector2> DefaultValue, ImGuiStyleVar Target, Vector2? Min = null, Vector2? Max = null, float Speed = 0.25f, string? Description = null);
|
||||||
|
|
||||||
|
private static LightlessConfigService? _config;
|
||||||
|
private static UiThemeConfigService? _themeConfig;
|
||||||
|
public static void Init(LightlessConfigService config, UiThemeConfigService themeConfig)
|
||||||
{
|
{
|
||||||
private static LightlessConfigService? _config;
|
_config = config;
|
||||||
public static void Init(LightlessConfigService config) => _config = config;
|
_themeConfig = themeConfig;
|
||||||
public static bool ShouldUseTheme => _config?.Current.UseLightlessRedesign ?? false;
|
|
||||||
|
|
||||||
private static bool _hasPushed;
|
|
||||||
private static int _pushedColorCount;
|
|
||||||
private static int _pushedStyleVarCount;
|
|
||||||
|
|
||||||
public static void PushStyle()
|
|
||||||
{
|
|
||||||
if (_hasPushed)
|
|
||||||
PopStyle();
|
|
||||||
|
|
||||||
if (!ShouldUseTheme)
|
|
||||||
{
|
|
||||||
_hasPushed = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_hasPushed = true;
|
|
||||||
_pushedColorCount = 0;
|
|
||||||
_pushedStyleVarCount = 0;
|
|
||||||
|
|
||||||
Push(ImGuiCol.Text, new Vector4(255, 255, 255, 255));
|
|
||||||
Push(ImGuiCol.TextDisabled, new Vector4(128, 128, 128, 255));
|
|
||||||
|
|
||||||
Push(ImGuiCol.WindowBg, new Vector4(23, 23, 23, 248));
|
|
||||||
Push(ImGuiCol.ChildBg, new Vector4(23, 23, 23, 66));
|
|
||||||
Push(ImGuiCol.PopupBg, new Vector4(23, 23, 23, 248));
|
|
||||||
|
|
||||||
Push(ImGuiCol.Border, new Vector4(65, 65, 65, 255));
|
|
||||||
Push(ImGuiCol.BorderShadow, new Vector4(0, 0, 0, 150));
|
|
||||||
|
|
||||||
Push(ImGuiCol.FrameBg, new Vector4(40, 40, 40, 255));
|
|
||||||
Push(ImGuiCol.FrameBgHovered, new Vector4(50, 50, 50, 255));
|
|
||||||
Push(ImGuiCol.FrameBgActive, new Vector4(30, 30, 30, 255));
|
|
||||||
|
|
||||||
Push(ImGuiCol.TitleBg, new Vector4(24, 24, 24, 232));
|
|
||||||
Push(ImGuiCol.TitleBgActive, new Vector4(30, 30, 30, 255));
|
|
||||||
Push(ImGuiCol.TitleBgCollapsed, new Vector4(27, 27, 27, 255));
|
|
||||||
|
|
||||||
Push(ImGuiCol.MenuBarBg, new Vector4(36, 36, 36, 255));
|
|
||||||
Push(ImGuiCol.ScrollbarBg, new Vector4(0, 0, 0, 0));
|
|
||||||
Push(ImGuiCol.ScrollbarGrab, new Vector4(62, 62, 62, 255));
|
|
||||||
Push(ImGuiCol.ScrollbarGrabHovered, new Vector4(70, 70, 70, 255));
|
|
||||||
Push(ImGuiCol.ScrollbarGrabActive, new Vector4(70, 70, 70, 255));
|
|
||||||
|
|
||||||
Push(ImGuiCol.CheckMark, UIColors.Get("LightlessPurple"));
|
|
||||||
|
|
||||||
Push(ImGuiCol.SliderGrab, new Vector4(101, 101, 101, 255));
|
|
||||||
Push(ImGuiCol.SliderGrabActive, new Vector4(123, 123, 123, 255));
|
|
||||||
|
|
||||||
Push(ImGuiCol.Button, UIColors.Get("ButtonDefault"));
|
|
||||||
Push(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurple"));
|
|
||||||
Push(ImGuiCol.ButtonActive, UIColors.Get("LightlessPurpleActive"));
|
|
||||||
|
|
||||||
Push(ImGuiCol.Header, new Vector4(0, 0, 0, 60));
|
|
||||||
Push(ImGuiCol.HeaderHovered, new Vector4(0, 0, 0, 90));
|
|
||||||
Push(ImGuiCol.HeaderActive, new Vector4(0, 0, 0, 120));
|
|
||||||
|
|
||||||
Push(ImGuiCol.Separator, new Vector4(75, 75, 75, 121));
|
|
||||||
Push(ImGuiCol.SeparatorHovered, UIColors.Get("LightlessPurple"));
|
|
||||||
Push(ImGuiCol.SeparatorActive, UIColors.Get("LightlessPurpleActive"));
|
|
||||||
|
|
||||||
Push(ImGuiCol.ResizeGrip, new Vector4(0, 0, 0, 0));
|
|
||||||
Push(ImGuiCol.ResizeGripHovered, new Vector4(0, 0, 0, 0));
|
|
||||||
Push(ImGuiCol.ResizeGripActive, UIColors.Get("LightlessPurpleActive"));
|
|
||||||
|
|
||||||
Push(ImGuiCol.Tab, new Vector4(40, 40, 40, 255));
|
|
||||||
Push(ImGuiCol.TabHovered, UIColors.Get("LightlessPurple"));
|
|
||||||
Push(ImGuiCol.TabActive, UIColors.Get("LightlessPurpleActive"));
|
|
||||||
Push(ImGuiCol.TabUnfocused, new Vector4(40, 40, 40, 255));
|
|
||||||
Push(ImGuiCol.TabUnfocusedActive, UIColors.Get("LightlessPurpleActive"));
|
|
||||||
|
|
||||||
Push(ImGuiCol.DockingPreview, UIColors.Get("LightlessPurpleActive"));
|
|
||||||
Push(ImGuiCol.DockingEmptyBg, new Vector4(50, 50, 50, 255));
|
|
||||||
|
|
||||||
Push(ImGuiCol.PlotLines, new Vector4(150, 150, 150, 255));
|
|
||||||
|
|
||||||
Push(ImGuiCol.TableHeaderBg, new Vector4(48, 48, 48, 255));
|
|
||||||
Push(ImGuiCol.TableBorderStrong, new Vector4(79, 79, 89, 255));
|
|
||||||
Push(ImGuiCol.TableBorderLight, new Vector4(59, 59, 64, 255));
|
|
||||||
Push(ImGuiCol.TableRowBg, new Vector4(0, 0, 0, 0));
|
|
||||||
Push(ImGuiCol.TableRowBgAlt, new Vector4(255, 255, 255, 15));
|
|
||||||
|
|
||||||
Push(ImGuiCol.TextSelectedBg, new Vector4(98, 75, 224, 255));
|
|
||||||
Push(ImGuiCol.DragDropTarget, new Vector4(98, 75, 224, 255));
|
|
||||||
|
|
||||||
Push(ImGuiCol.NavHighlight, new Vector4(98, 75, 224, 179));
|
|
||||||
Push(ImGuiCol.NavWindowingDimBg, new Vector4(204, 204, 204, 51));
|
|
||||||
Push(ImGuiCol.NavWindowingHighlight, new Vector4(204, 204, 204, 89));
|
|
||||||
|
|
||||||
PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(6, 6));
|
|
||||||
PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(4, 3));
|
|
||||||
PushStyleVar(ImGuiStyleVar.CellPadding, new Vector2(4, 4));
|
|
||||||
PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(4, 4));
|
|
||||||
PushStyleVar(ImGuiStyleVar.ItemInnerSpacing, new Vector2(4, 4));
|
|
||||||
|
|
||||||
PushStyleVar(ImGuiStyleVar.IndentSpacing, 21.0f);
|
|
||||||
PushStyleVar(ImGuiStyleVar.ScrollbarSize, 10.0f);
|
|
||||||
PushStyleVar(ImGuiStyleVar.GrabMinSize, 20.0f);
|
|
||||||
|
|
||||||
PushStyleVar(ImGuiStyleVar.WindowBorderSize, 1.5f);
|
|
||||||
PushStyleVar(ImGuiStyleVar.ChildBorderSize, 1.5f);
|
|
||||||
PushStyleVar(ImGuiStyleVar.PopupBorderSize, 1.5f);
|
|
||||||
PushStyleVar(ImGuiStyleVar.FrameBorderSize, 0f);
|
|
||||||
|
|
||||||
PushStyleVar(ImGuiStyleVar.WindowRounding, 7f);
|
|
||||||
PushStyleVar(ImGuiStyleVar.ChildRounding, 4f);
|
|
||||||
PushStyleVar(ImGuiStyleVar.FrameRounding, 4f);
|
|
||||||
PushStyleVar(ImGuiStyleVar.PopupRounding, 4f);
|
|
||||||
PushStyleVar(ImGuiStyleVar.ScrollbarRounding, 4f);
|
|
||||||
PushStyleVar(ImGuiStyleVar.GrabRounding, 4f);
|
|
||||||
PushStyleVar(ImGuiStyleVar.TabRounding, 4f);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void PopStyle()
|
|
||||||
{
|
|
||||||
if (!_hasPushed)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (_pushedStyleVarCount > 0)
|
|
||||||
ImGui.PopStyleVar(_pushedStyleVarCount);
|
|
||||||
if (_pushedColorCount > 0)
|
|
||||||
ImGui.PopStyleColor(_pushedColorCount);
|
|
||||||
|
|
||||||
_hasPushed = false;
|
|
||||||
_pushedColorCount = 0;
|
|
||||||
_pushedStyleVarCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void Push(ImGuiCol col, Vector4 rgba)
|
|
||||||
{
|
|
||||||
if (rgba.X > 1f || rgba.Y > 1f || rgba.Z > 1f || rgba.W > 1f)
|
|
||||||
rgba /= 255f;
|
|
||||||
|
|
||||||
ImGui.PushStyleColor(col, rgba);
|
|
||||||
_pushedColorCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void Push(ImGuiCol col, uint packedRgba)
|
|
||||||
{
|
|
||||||
ImGui.PushStyleColor(col, packedRgba);
|
|
||||||
_pushedColorCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void PushStyleVar(ImGuiStyleVar var, float value)
|
|
||||||
{
|
|
||||||
ImGui.PushStyleVar(var, value);
|
|
||||||
_pushedStyleVarCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void PushStyleVar(ImGuiStyleVar var, Vector2 value)
|
|
||||||
{
|
|
||||||
ImGui.PushStyleVar(var, value);
|
|
||||||
_pushedStyleVarCount++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
public static bool ShouldUseTheme => _config?.Current.UseLightlessRedesign ?? false;
|
||||||
|
|
||||||
|
private static bool _hasPushed;
|
||||||
|
private static int _pushedColorCount;
|
||||||
|
private static int _pushedStyleVarCount;
|
||||||
|
|
||||||
|
private static readonly StyleColorOption[] _colorOptions =
|
||||||
|
[
|
||||||
|
new("color.text", "Text", () => Rgba(255, 255, 255, 255), ImGuiCol.Text),
|
||||||
|
new("color.textDisabled", "Text (Disabled)", () => Rgba(128, 128, 128, 255), ImGuiCol.TextDisabled),
|
||||||
|
new("color.windowBg", "Window Background", () => Rgba(23, 23, 23, 248), ImGuiCol.WindowBg),
|
||||||
|
new("color.childBg", "Child Background", () => Rgba(23, 23, 23, 66), ImGuiCol.ChildBg),
|
||||||
|
new("color.popupBg", "Popup Background", () => Rgba(23, 23, 23, 248), ImGuiCol.PopupBg),
|
||||||
|
new("color.border", "Border", () => Rgba(65, 65, 65, 255), ImGuiCol.Border),
|
||||||
|
new("color.borderShadow", "Border Shadow", () => Rgba(0, 0, 0, 150), ImGuiCol.BorderShadow),
|
||||||
|
new("color.frameBg", "Frame Background", () => Rgba(40, 40, 40, 255), ImGuiCol.FrameBg),
|
||||||
|
new("color.frameBgHovered", "Frame Background (Hover)", () => Rgba(50, 50, 50, 255), ImGuiCol.FrameBgHovered),
|
||||||
|
new("color.frameBgActive", "Frame Background (Active)", () => Rgba(30, 30, 30, 255), ImGuiCol.FrameBgActive),
|
||||||
|
new("color.titleBg", "Title Background", () => Rgba(24, 24, 24, 232), ImGuiCol.TitleBg),
|
||||||
|
new("color.titleBgActive", "Title Background (Active)", () => Rgba(30, 30, 30, 255), ImGuiCol.TitleBgActive),
|
||||||
|
new("color.titleBgCollapsed", "Title Background (Collapsed)", () => Rgba(27, 27, 27, 255), ImGuiCol.TitleBgCollapsed),
|
||||||
|
new("color.menuBarBg", "Menu Bar Background", () => Rgba(36, 36, 36, 255), ImGuiCol.MenuBarBg),
|
||||||
|
new("color.scrollbarBg", "Scrollbar Background", () => Rgba(0, 0, 0, 0), ImGuiCol.ScrollbarBg),
|
||||||
|
new("color.scrollbarGrab", "Scrollbar Grab", () => Rgba(62, 62, 62, 255), ImGuiCol.ScrollbarGrab),
|
||||||
|
new("color.scrollbarGrabHovered", "Scrollbar Grab (Hover)", () => Rgba(70, 70, 70, 255), ImGuiCol.ScrollbarGrabHovered),
|
||||||
|
new("color.scrollbarGrabActive", "Scrollbar Grab (Active)", () => Rgba(70, 70, 70, 255), ImGuiCol.ScrollbarGrabActive),
|
||||||
|
new("color.checkMark", "Check Mark", () => UIColors.Get("LightlessPurple"), ImGuiCol.CheckMark, UiColorKey: "LightlessPurple"),
|
||||||
|
new("color.sliderGrab", "Slider Grab", () => Rgba(101, 101, 101, 255), ImGuiCol.SliderGrab),
|
||||||
|
new("color.sliderGrabActive", "Slider Grab (Active)", () => Rgba(123, 123, 123, 255), ImGuiCol.SliderGrabActive),
|
||||||
|
new("color.button", "Button", () => UIColors.Get("ButtonDefault"), ImGuiCol.Button, UiColorKey: "ButtonDefault"),
|
||||||
|
new("color.buttonHovered", "Button (Hover)", () => UIColors.Get("LightlessPurple"), ImGuiCol.ButtonHovered, UiColorKey: "LightlessPurple"),
|
||||||
|
new("color.buttonActive", "Button (Active)", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.ButtonActive, UiColorKey: "LightlessPurpleActive"),
|
||||||
|
new("color.header", "Header", () => Rgba(0, 0, 0, 60), ImGuiCol.Header),
|
||||||
|
new("color.headerHovered", "Header (Hover)", () => Rgba(0, 0, 0, 90), ImGuiCol.HeaderHovered),
|
||||||
|
new("color.headerActive", "Header (Active)", () => Rgba(0, 0, 0, 120), ImGuiCol.HeaderActive),
|
||||||
|
new("color.separator", "Separator", () => Rgba(75, 75, 75, 121), ImGuiCol.Separator),
|
||||||
|
new("color.separatorHovered", "Separator (Hover)", () => UIColors.Get("LightlessPurple"), ImGuiCol.SeparatorHovered, UiColorKey: "LightlessPurple"),
|
||||||
|
new("color.separatorActive", "Separator (Active)", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.SeparatorActive, UiColorKey: "LightlessPurpleActive"),
|
||||||
|
new("color.resizeGrip", "Resize Grip", () => Rgba(0, 0, 0, 0), ImGuiCol.ResizeGrip),
|
||||||
|
new("color.resizeGripHovered", "Resize Grip (Hover)", () => Rgba(0, 0, 0, 0), ImGuiCol.ResizeGripHovered),
|
||||||
|
new("color.resizeGripActive", "Resize Grip (Active)", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.ResizeGripActive, UiColorKey: "LightlessPurpleActive"),
|
||||||
|
new("color.tab", "Tab", () => Rgba(40, 40, 40, 255), ImGuiCol.Tab),
|
||||||
|
new("color.tabHovered", "Tab (Hover)", () => UIColors.Get("LightlessPurple"), ImGuiCol.TabHovered, UiColorKey: "LightlessPurple"),
|
||||||
|
new("color.tabActive", "Tab (Active)", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.TabActive, UiColorKey: "LightlessPurpleActive"),
|
||||||
|
new("color.tabUnfocused", "Tab (Unfocused)", () => Rgba(40, 40, 40, 255), ImGuiCol.TabUnfocused),
|
||||||
|
new("color.tabUnfocusedActive", "Tab (Unfocused Active)", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.TabUnfocusedActive, UiColorKey: "LightlessPurpleActive"),
|
||||||
|
new("color.dockingPreview", "Docking Preview", () => UIColors.Get("LightlessPurpleActive"), ImGuiCol.DockingPreview, UiColorKey: "LightlessPurpleActive"),
|
||||||
|
new("color.dockingEmptyBg", "Docking Empty Background", () => Rgba(50, 50, 50, 255), ImGuiCol.DockingEmptyBg),
|
||||||
|
new("color.plotLines", "Plot Lines", () => Rgba(150, 150, 150, 255), ImGuiCol.PlotLines),
|
||||||
|
new("color.tableHeaderBg", "Table Header Background", () => Rgba(48, 48, 48, 255), ImGuiCol.TableHeaderBg),
|
||||||
|
new("color.tableBorderStrong", "Table Border Strong", () => Rgba(79, 79, 89, 255), ImGuiCol.TableBorderStrong),
|
||||||
|
new("color.tableBorderLight", "Table Border Light", () => Rgba(59, 59, 64, 255), ImGuiCol.TableBorderLight),
|
||||||
|
new("color.tableRowBg", "Table Row Background", () => Rgba(0, 0, 0, 0), ImGuiCol.TableRowBg),
|
||||||
|
new("color.tableRowBgAlt", "Table Row Background (Alt)", () => Rgba(255, 255, 255, 15), ImGuiCol.TableRowBgAlt),
|
||||||
|
new("color.textSelectedBg", "Text Selection Background", () => Rgba(173, 138, 245, 255), ImGuiCol.TextSelectedBg),
|
||||||
|
new("color.dragDropTarget", "Drag & Drop Target", () => Rgba(173, 138, 245, 255), ImGuiCol.DragDropTarget),
|
||||||
|
new("color.navHighlight", "Navigation Highlight", () => Rgba(173, 138, 245, 179), ImGuiCol.NavHighlight),
|
||||||
|
new("color.navWindowingDimBg", "Navigation Window Dim", () => Rgba(204, 204, 204, 51), ImGuiCol.NavWindowingDimBg),
|
||||||
|
new("color.navWindowingHighlight", "Navigation Window Highlight", () => Rgba(204, 204, 204, 89), ImGuiCol.NavWindowingHighlight)
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly StyleVector2Option[] _vector2Options =
|
||||||
|
[
|
||||||
|
new("vector.windowPadding", "Window Padding", () => new Vector2(6f, 6f), ImGuiStyleVar.WindowPadding),
|
||||||
|
new("vector.framePadding", "Frame Padding", () => new Vector2(4f, 3f), ImGuiStyleVar.FramePadding),
|
||||||
|
new("vector.cellPadding", "Cell Padding", () => new Vector2(4f, 4f), ImGuiStyleVar.CellPadding),
|
||||||
|
new("vector.itemSpacing", "Item Spacing", () => new Vector2(4f, 4f), ImGuiStyleVar.ItemSpacing),
|
||||||
|
new("vector.itemInnerSpacing", "Item Inner Spacing", () => new Vector2(4f, 4f), ImGuiStyleVar.ItemInnerSpacing)
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly StyleFloatOption[] _floatOptions =
|
||||||
|
[
|
||||||
|
new("float.indentSpacing", "Indent Spacing", 21f, ImGuiStyleVar.IndentSpacing, 0f, 100f, 0.5f),
|
||||||
|
new("float.scrollbarSize", "Scrollbar Size", 10f, ImGuiStyleVar.ScrollbarSize, 4f, 30f, 0.5f),
|
||||||
|
new("float.grabMinSize", "Grab Minimum Size", 20f, ImGuiStyleVar.GrabMinSize, 1f, 80f, 0.5f),
|
||||||
|
new("float.windowBorderSize", "Window Border Size", 1.5f, ImGuiStyleVar.WindowBorderSize, 0f, 5f, 0.1f),
|
||||||
|
new("float.childBorderSize", "Child Border Size", 1.5f, ImGuiStyleVar.ChildBorderSize, 0f, 5f, 0.1f),
|
||||||
|
new("float.popupBorderSize", "Popup Border Size", 1.5f, ImGuiStyleVar.PopupBorderSize, 0f, 5f, 0.1f),
|
||||||
|
new("float.frameBorderSize", "Frame Border Size", 0f, ImGuiStyleVar.FrameBorderSize, 0f, 5f, 0.1f),
|
||||||
|
new("float.windowRounding", "Window Rounding", 7f, ImGuiStyleVar.WindowRounding, 0f, 20f, 0.2f),
|
||||||
|
new("float.childRounding", "Child Rounding", 4f, ImGuiStyleVar.ChildRounding, 0f, 20f, 0.2f),
|
||||||
|
new("float.frameRounding", "Frame Rounding", 4f, ImGuiStyleVar.FrameRounding, 0f, 20f, 0.2f),
|
||||||
|
new("float.popupRounding", "Popup Rounding", 4f, ImGuiStyleVar.PopupRounding, 0f, 20f, 0.2f),
|
||||||
|
new("float.scrollbarRounding", "Scrollbar Rounding", 4f, ImGuiStyleVar.ScrollbarRounding, 0f, 20f, 0.2f),
|
||||||
|
new("float.grabRounding", "Grab Rounding", 4f, ImGuiStyleVar.GrabRounding, 0f, 20f, 0.2f),
|
||||||
|
new("float.tabRounding", "Tab Rounding", 4f, ImGuiStyleVar.TabRounding, 0f, 20f, 0.2f)
|
||||||
|
];
|
||||||
|
|
||||||
|
public static IReadOnlyList<StyleColorOption> ColorOptions => _colorOptions;
|
||||||
|
public static IReadOnlyList<StyleFloatOption> FloatOptions => _floatOptions;
|
||||||
|
public static IReadOnlyList<StyleVector2Option> Vector2Options => _vector2Options;
|
||||||
|
|
||||||
|
public static void PushStyle()
|
||||||
|
{
|
||||||
|
if (_hasPushed)
|
||||||
|
PopStyle();
|
||||||
|
|
||||||
|
if (!ShouldUseTheme)
|
||||||
|
{
|
||||||
|
_hasPushed = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_hasPushed = true;
|
||||||
|
_pushedColorCount = 0;
|
||||||
|
_pushedStyleVarCount = 0;
|
||||||
|
|
||||||
|
foreach (var option in _colorOptions)
|
||||||
|
Push(option.Target, ResolveColor(option));
|
||||||
|
|
||||||
|
foreach (var option in _vector2Options)
|
||||||
|
PushStyleVar(option.Target, ResolveVector(option));
|
||||||
|
|
||||||
|
foreach (var option in _floatOptions)
|
||||||
|
PushStyleVar(option.Target, ResolveFloat(option));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void PopStyle()
|
||||||
|
{
|
||||||
|
if (!_hasPushed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_pushedStyleVarCount > 0)
|
||||||
|
ImGui.PopStyleVar(_pushedStyleVarCount);
|
||||||
|
if (_pushedColorCount > 0)
|
||||||
|
ImGui.PopStyleColor(_pushedColorCount);
|
||||||
|
|
||||||
|
_hasPushed = false;
|
||||||
|
_pushedColorCount = 0;
|
||||||
|
_pushedStyleVarCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector4 ResolveColor(StyleColorOption option)
|
||||||
|
{
|
||||||
|
var defaultValue = NormalizeColorVector(option.DefaultValue());
|
||||||
|
if (_themeConfig?.Current.StyleOverrides.TryGetValue(option.Key, out var overrideValue) == true && overrideValue.Color is { } packed)
|
||||||
|
return PackedColorToVector4(packed);
|
||||||
|
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector2 ResolveVector(StyleVector2Option option)
|
||||||
|
{
|
||||||
|
var value = option.DefaultValue();
|
||||||
|
if (_themeConfig?.Current.StyleOverrides.TryGetValue(option.Key, out var overrideValue) == true && overrideValue.Vector2 is { } vectorOverride)
|
||||||
|
{
|
||||||
|
value = vectorOverride;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option.Min is { } min)
|
||||||
|
value = Vector2.Max(value, min);
|
||||||
|
if (option.Max is { } max)
|
||||||
|
value = Vector2.Min(value, max);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float ResolveFloat(StyleFloatOption option)
|
||||||
|
{
|
||||||
|
var value = option.DefaultValue;
|
||||||
|
if (_themeConfig?.Current.StyleOverrides.TryGetValue(option.Key, out var overrideValue) == true && overrideValue.Float is { } floatOverride)
|
||||||
|
{
|
||||||
|
value = floatOverride;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option.Min.HasValue)
|
||||||
|
value = MathF.Max(option.Min.Value, value);
|
||||||
|
if (option.Max.HasValue)
|
||||||
|
value = MathF.Min(option.Max.Value, value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Push(ImGuiCol col, Vector4 rgba)
|
||||||
|
{
|
||||||
|
rgba = NormalizeColorVector(rgba);
|
||||||
|
ImGui.PushStyleColor(col, rgba);
|
||||||
|
_pushedColorCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PushStyleVar(ImGuiStyleVar var, float value)
|
||||||
|
{
|
||||||
|
ImGui.PushStyleVar(var, value);
|
||||||
|
_pushedStyleVarCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PushStyleVar(ImGuiStyleVar var, Vector2 value)
|
||||||
|
{
|
||||||
|
ImGui.PushStyleVar(var, value);
|
||||||
|
_pushedStyleVarCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector4 Rgba(byte r, byte g, byte b, byte a = 255)
|
||||||
|
=> new Vector4(r / 255f, g / 255f, b / 255f, a / 255f);
|
||||||
|
|
||||||
|
internal static Vector4 NormalizeColorVector(Vector4 rgba)
|
||||||
|
{
|
||||||
|
if (rgba.X > 1f || rgba.Y > 1f || rgba.Z > 1f || rgba.W > 1f)
|
||||||
|
rgba /= 255f;
|
||||||
|
return rgba;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Vector4 PackedColorToVector4(uint color)
|
||||||
|
=> new(
|
||||||
|
(color & 0xFF) / 255f,
|
||||||
|
((color >> 8) & 0xFF) / 255f,
|
||||||
|
((color >> 16) & 0xFF) / 255f,
|
||||||
|
((color >> 24) & 0xFF) / 255f);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Colors;
|
using Dalamud.Interface.Colors;
|
||||||
|
using Dalamud.Interface.ImGuiFileDialog;
|
||||||
|
using Dalamud.Interface.Textures.TextureWraps;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.API.Data.Enum;
|
using LightlessSync.API.Data.Enum;
|
||||||
using LightlessSync.API.Data.Extensions;
|
using LightlessSync.API.Data.Extensions;
|
||||||
using LightlessSync.API.Dto.Group;
|
using LightlessSync.API.Dto.Group;
|
||||||
|
using LightlessSync.API.Dto.User;
|
||||||
using LightlessSync.PlayerData.Pairs;
|
using LightlessSync.PlayerData.Pairs;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
|
using LightlessSync.UI.Handlers;
|
||||||
using LightlessSync.WebAPI;
|
using LightlessSync.WebAPI;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
namespace LightlessSync.UI;
|
namespace LightlessSync.UI;
|
||||||
|
|
||||||
@@ -22,29 +31,51 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
private readonly bool _isOwner = false;
|
private readonly bool _isOwner = false;
|
||||||
private readonly List<string> _oneTimeInvites = [];
|
private readonly List<string> _oneTimeInvites = [];
|
||||||
private readonly PairManager _pairManager;
|
private readonly PairManager _pairManager;
|
||||||
|
private readonly LightlessProfileManager _lightlessProfileManager;
|
||||||
|
private readonly FileDialogManager _fileDialogManager;
|
||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
private List<BannedGroupUserDto> _bannedUsers = [];
|
private List<BannedGroupUserDto> _bannedUsers = [];
|
||||||
|
private LightlessGroupProfileData? _profileData = null;
|
||||||
|
private bool _adjustedForScollBarsLocalProfile = false;
|
||||||
|
private bool _adjustedForScollBarsOnlineProfile = false;
|
||||||
|
private string _descriptionText = string.Empty;
|
||||||
|
private IDalamudTextureWrap? _pfpTextureWrap;
|
||||||
|
private string _profileDescription = string.Empty;
|
||||||
|
private byte[] _profileImage = [];
|
||||||
|
private bool _showFileDialogError = false;
|
||||||
private int _multiInvites;
|
private int _multiInvites;
|
||||||
private string _newPassword;
|
private string _newPassword;
|
||||||
private bool _pwChangeSuccess;
|
private bool _pwChangeSuccess;
|
||||||
private Task<int>? _pruneTestTask;
|
private Task<int>? _pruneTestTask;
|
||||||
private Task<int>? _pruneTask;
|
private Task<int>? _pruneTask;
|
||||||
private int _pruneDays = 14;
|
private int _pruneDays = 14;
|
||||||
|
private List<int> _selectedTags = [];
|
||||||
|
|
||||||
public SyncshellAdminUI(ILogger<SyncshellAdminUI> logger, LightlessMediator mediator, ApiController apiController,
|
public SyncshellAdminUI(ILogger<SyncshellAdminUI> logger, LightlessMediator mediator, ApiController apiController,
|
||||||
UiSharedService uiSharedService, PairManager pairManager, GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService)
|
UiSharedService uiSharedService, PairManager pairManager, GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService, LightlessProfileManager lightlessProfileManager, FileDialogManager fileDialogManager)
|
||||||
: base(logger, mediator, "Syncshell Admin Panel (" + groupFullInfo.GroupAliasOrGID + ")", performanceCollectorService)
|
: base(logger, mediator, "Syncshell Admin Panel (" + groupFullInfo.GroupAliasOrGID + ")", performanceCollectorService)
|
||||||
{
|
{
|
||||||
GroupFullInfo = groupFullInfo;
|
GroupFullInfo = groupFullInfo;
|
||||||
_apiController = apiController;
|
_apiController = apiController;
|
||||||
_uiSharedService = uiSharedService;
|
_uiSharedService = uiSharedService;
|
||||||
_pairManager = pairManager;
|
_pairManager = pairManager;
|
||||||
|
_lightlessProfileManager = lightlessProfileManager;
|
||||||
|
_fileDialogManager = fileDialogManager;
|
||||||
|
|
||||||
_isOwner = string.Equals(GroupFullInfo.OwnerUID, _apiController.UID, StringComparison.Ordinal);
|
_isOwner = string.Equals(GroupFullInfo.OwnerUID, _apiController.UID, StringComparison.Ordinal);
|
||||||
_isModerator = GroupFullInfo.GroupUserInfo.IsModerator();
|
_isModerator = GroupFullInfo.GroupUserInfo.IsModerator();
|
||||||
_newPassword = string.Empty;
|
_newPassword = string.Empty;
|
||||||
_multiInvites = 30;
|
_multiInvites = 30;
|
||||||
_pwChangeSuccess = true;
|
_pwChangeSuccess = true;
|
||||||
IsOpen = true;
|
IsOpen = true;
|
||||||
|
Mediator.Subscribe<ClearProfileGroupDataMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
if (msg.GroupData == null || string.Equals(msg.GroupData.AliasOrGID, GroupFullInfo.Group.AliasOrGID, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_pfpTextureWrap?.Dispose();
|
||||||
|
_pfpTextureWrap = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
SizeConstraints = new WindowSizeConstraints()
|
SizeConstraints = new WindowSizeConstraints()
|
||||||
{
|
{
|
||||||
MinimumSize = new(700, 500),
|
MinimumSize = new(700, 500),
|
||||||
@@ -58,10 +89,13 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (!_isModerator && !_isOwner) return;
|
if (!_isModerator && !_isOwner) return;
|
||||||
|
|
||||||
|
_logger.LogTrace("Drawing Syncshell Admin UI for {group}", GroupFullInfo.GroupAliasOrGID);
|
||||||
GroupFullInfo = _pairManager.Groups[GroupFullInfo.Group];
|
GroupFullInfo = _pairManager.Groups[GroupFullInfo.Group];
|
||||||
|
|
||||||
using var id = ImRaii.PushId("syncshell_admin_" + GroupFullInfo.GID);
|
_profileData = _lightlessProfileManager.GetLightlessGroupProfile(GroupFullInfo.Group);
|
||||||
|
GetTagsFromProfile();
|
||||||
|
|
||||||
|
using var id = ImRaii.PushId("syncshell_admin_" + GroupFullInfo.GID);
|
||||||
using (_uiSharedService.UidFont.Push())
|
using (_uiSharedService.UidFont.Push())
|
||||||
_uiSharedService.UnderlinedBigText(GroupFullInfo.GroupAliasOrGID + " Administrative Panel", UIColors.Get("LightlessBlue"));
|
_uiSharedService.UnderlinedBigText(GroupFullInfo.GroupAliasOrGID + " Administrative Panel", UIColors.Get("LightlessBlue"));
|
||||||
|
|
||||||
@@ -69,14 +103,16 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
var perm = GroupFullInfo.GroupPermissions;
|
var perm = GroupFullInfo.GroupPermissions;
|
||||||
|
|
||||||
using var tabbar = ImRaii.TabBar("syncshell_tab_" + GroupFullInfo.GID);
|
using var tabbar = ImRaii.TabBar("syncshell_tab_" + GroupFullInfo.GID);
|
||||||
|
|
||||||
if (tabbar)
|
if (tabbar)
|
||||||
{
|
{
|
||||||
DrawInvites(perm);
|
DrawInvites(perm);
|
||||||
|
|
||||||
DrawManagement();
|
DrawManagement();
|
||||||
|
|
||||||
DrawPermission(perm);
|
DrawPermission(perm);
|
||||||
|
|
||||||
|
DrawProfile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +212,184 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
ownerTab.Dispose();
|
ownerTab.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private void DrawProfile()
|
||||||
|
{
|
||||||
|
var profileTab = ImRaii.TabItem("Profile");
|
||||||
|
|
||||||
|
if (profileTab)
|
||||||
|
{
|
||||||
|
if (_uiSharedService.MediumTreeNode("Current Profile", UIColors.Get("LightlessPurple")))
|
||||||
|
{
|
||||||
|
ImGui.Dummy(new Vector2(5));
|
||||||
|
|
||||||
|
if (!_profileImage.SequenceEqual(_profileData.ImageData.Value))
|
||||||
|
{
|
||||||
|
_profileImage = _profileData.ImageData.Value;
|
||||||
|
_pfpTextureWrap?.Dispose();
|
||||||
|
_pfpTextureWrap = _uiSharedService.LoadImage(_profileImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(_profileDescription, _profileData.Description, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_profileDescription = _profileData.Description;
|
||||||
|
_descriptionText = _profileDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_pfpTextureWrap != null)
|
||||||
|
{
|
||||||
|
ImGui.Image(_pfpTextureWrap.Handle, ImGuiHelpers.ScaledVector2(_pfpTextureWrap.Width, _pfpTextureWrap.Height));
|
||||||
|
}
|
||||||
|
|
||||||
|
var spacing = ImGui.GetStyle().ItemSpacing.X;
|
||||||
|
ImGuiHelpers.ScaledRelativeSameLine(256, spacing);
|
||||||
|
using (_uiSharedService.GameFont.Push())
|
||||||
|
{
|
||||||
|
var descriptionTextSize = ImGui.CalcTextSize(_profileData.Description, wrapWidth: 256f);
|
||||||
|
var childFrame = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 256);
|
||||||
|
if (descriptionTextSize.Y > childFrame.Y)
|
||||||
|
{
|
||||||
|
_adjustedForScollBarsOnlineProfile = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_adjustedForScollBarsOnlineProfile = false;
|
||||||
|
}
|
||||||
|
childFrame = childFrame with
|
||||||
|
{
|
||||||
|
X = childFrame.X + (_adjustedForScollBarsOnlineProfile ? ImGui.GetStyle().ScrollbarSize : 0),
|
||||||
|
};
|
||||||
|
if (ImGui.BeginChildFrame(101, childFrame))
|
||||||
|
{
|
||||||
|
UiSharedService.TextWrapped(_profileData.Description);
|
||||||
|
}
|
||||||
|
ImGui.EndChildFrame();
|
||||||
|
ImGui.TreePop();
|
||||||
|
}
|
||||||
|
var nsfw = _profileData.IsNsfw;
|
||||||
|
ImGui.BeginDisabled();
|
||||||
|
ImGui.Checkbox("Is NSFW", ref nsfw);
|
||||||
|
ImGui.EndDisabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Separator();
|
||||||
|
|
||||||
|
if (_uiSharedService.MediumTreeNode("Profile Settings", UIColors.Get("LightlessPurple")))
|
||||||
|
{
|
||||||
|
ImGui.Dummy(new Vector2(5));
|
||||||
|
ImGui.TextUnformatted($"Profile Picture:");
|
||||||
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture"))
|
||||||
|
{
|
||||||
|
_fileDialogManager.OpenFileDialog("Select new Profile picture", ".png", (success, file) =>
|
||||||
|
{
|
||||||
|
if (!success) return;
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var fileContent = await File.ReadAllBytesAsync(file).ConfigureAwait(false);
|
||||||
|
MemoryStream ms = new(fileContent);
|
||||||
|
await using (ms.ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
var format = await Image.DetectFormatAsync(ms).ConfigureAwait(false);
|
||||||
|
if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_showFileDialogError = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
using var image = Image.Load<Rgba32>(fileContent);
|
||||||
|
|
||||||
|
if (image.Width > 512 || image.Height > 512 || (fileContent.Length > 2000 * 1024))
|
||||||
|
{
|
||||||
|
_showFileDialogError = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_showFileDialogError = false;
|
||||||
|
await _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: null, Convert.ToBase64String(fileContent), IsNsfw: null, IsDisabled: null))
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Select and upload a new profile picture");
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear uploaded profile picture"))
|
||||||
|
{
|
||||||
|
_ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: null, PictureBase64: null, IsNsfw: null, IsDisabled: null));
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Clear your currently uploaded profile picture");
|
||||||
|
if (_showFileDialogError)
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped("The profile picture must be a PNG file with a maximum height and width of 256px and 250KiB size", ImGuiColors.DalamudRed);
|
||||||
|
}
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.TextUnformatted($"Tags:");
|
||||||
|
var childFrameLocal = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 200);
|
||||||
|
|
||||||
|
var allCategoryIndexes = Enum.GetValues<ProfileTags>()
|
||||||
|
.Cast<int>()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach(int tag in allCategoryIndexes)
|
||||||
|
{
|
||||||
|
using (ImRaii.PushId($"tag-{tag}")) DrawTag(tag);
|
||||||
|
}
|
||||||
|
ImGui.Separator();
|
||||||
|
var widthTextBox = 400;
|
||||||
|
var posX = ImGui.GetCursorPosX();
|
||||||
|
ImGui.TextUnformatted($"Description {_descriptionText.Length}/1500");
|
||||||
|
ImGui.SetCursorPosX(posX);
|
||||||
|
ImGuiHelpers.ScaledRelativeSameLine(widthTextBox, ImGui.GetStyle().ItemSpacing.X);
|
||||||
|
ImGui.TextUnformatted("Preview (approximate)");
|
||||||
|
using (_uiSharedService.GameFont.Push())
|
||||||
|
ImGui.InputTextMultiline("##description", ref _descriptionText, 1500, ImGuiHelpers.ScaledVector2(widthTextBox, 200));
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
|
||||||
|
using (_uiSharedService.GameFont.Push())
|
||||||
|
{
|
||||||
|
var descriptionTextSizeLocal = ImGui.CalcTextSize(_descriptionText, wrapWidth: 256f);
|
||||||
|
if (descriptionTextSizeLocal.Y > childFrameLocal.Y)
|
||||||
|
{
|
||||||
|
_adjustedForScollBarsLocalProfile = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_adjustedForScollBarsLocalProfile = false;
|
||||||
|
}
|
||||||
|
childFrameLocal = childFrameLocal with
|
||||||
|
{
|
||||||
|
X = childFrameLocal.X + (_adjustedForScollBarsLocalProfile ? ImGui.GetStyle().ScrollbarSize : 0),
|
||||||
|
};
|
||||||
|
if (ImGui.BeginChildFrame(102, childFrameLocal))
|
||||||
|
{
|
||||||
|
UiSharedService.TextWrapped(_descriptionText);
|
||||||
|
}
|
||||||
|
ImGui.EndChildFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Description"))
|
||||||
|
{
|
||||||
|
_ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: _descriptionText, Tags: null, PictureBase64: null, IsNsfw: null, IsDisabled: null));
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Sets your profile description text");
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear Description"))
|
||||||
|
{
|
||||||
|
_ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: null, PictureBase64: null, IsNsfw: null, IsDisabled: null));
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Clears your profile description text");
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.TextUnformatted($"Profile Options:");
|
||||||
|
var isNsfw = _profileData.IsNsfw;
|
||||||
|
if (ImGui.Checkbox("Profile is NSFW", ref isNsfw))
|
||||||
|
{
|
||||||
|
_ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: null, PictureBase64: null, IsNsfw: isNsfw, IsDisabled: null));
|
||||||
|
}
|
||||||
|
_uiSharedService.DrawHelpText("If your profile description or image can be considered NSFW, toggle this to ON");
|
||||||
|
ImGui.TreePop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
profileTab.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawManagement()
|
private void DrawManagement()
|
||||||
{
|
{
|
||||||
@@ -192,7 +406,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
var tableFlags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingStretchProp;
|
var tableFlags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingStretchProp;
|
||||||
if (pairs.Count > 10) tableFlags |= ImGuiTableFlags.ScrollY;
|
if (pairs.Count > 10) tableFlags |= ImGuiTableFlags.ScrollY;
|
||||||
using var table = ImRaii.Table("userList#" + GroupFullInfo.Group.GID, 3, tableFlags);
|
using var table = ImRaii.Table("userList#" + GroupFullInfo.Group.AliasOrGID, 3, tableFlags);
|
||||||
if (table)
|
if (table)
|
||||||
{
|
{
|
||||||
ImGui.TableSetupColumn("Alias/UID/Note", ImGuiTableColumnFlags.None, 4);
|
ImGui.TableSetupColumn("Alias/UID/Note", ImGuiTableColumnFlags.None, 4);
|
||||||
@@ -474,7 +688,6 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
}
|
}
|
||||||
mgmtTab.Dispose();
|
mgmtTab.Dispose();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawInvites(GroupPermissions perm)
|
private void DrawInvites(GroupPermissions perm)
|
||||||
@@ -521,9 +734,37 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
inviteTab.Dispose();
|
inviteTab.Dispose();
|
||||||
}
|
}
|
||||||
|
private void DrawTag(int tag)
|
||||||
|
{
|
||||||
|
var HasTag = _selectedTags.Contains(tag);
|
||||||
|
var tagName = (ProfileTags)tag;
|
||||||
|
|
||||||
|
if (ImGui.Checkbox(tagName.ToString(), ref HasTag))
|
||||||
|
{
|
||||||
|
if (HasTag)
|
||||||
|
{
|
||||||
|
_selectedTags.Add(tag);
|
||||||
|
_ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: _selectedTags.ToArray(), PictureBase64: null, IsNsfw: null, IsDisabled: null));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_selectedTags.Remove(tag);
|
||||||
|
_ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: _selectedTags.ToArray(), PictureBase64: null, IsNsfw: null, IsDisabled: null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GetTagsFromProfile()
|
||||||
|
{
|
||||||
|
if (_profileData != null)
|
||||||
|
{
|
||||||
|
_selectedTags = [.. _profileData.Tags];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public override void OnClose()
|
public override void OnClose()
|
||||||
{
|
{
|
||||||
Mediator.Publish(new RemoveWindowMessage(this));
|
Mediator.Publish(new RemoveWindowMessage(this));
|
||||||
|
_pfpTextureWrap?.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
ImGuiHelpers.ScaledDummy(0.5f);
|
ImGuiHelpers.ScaledDummy(0.5f);
|
||||||
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 10.0f);
|
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 10.0f);
|
||||||
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessYellow2"));
|
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("PairBlue"));
|
||||||
|
|
||||||
if (ImGui.Button("Open Lightfinder", new Vector2(200 * ImGuiHelpers.GlobalScale, 0)))
|
if (ImGui.Button("Open Lightfinder", new Vector2(200 * ImGuiHelpers.GlobalScale, 0)))
|
||||||
{
|
{
|
||||||
@@ -288,8 +288,6 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentGids = _nearbySyncshells.Select(s => s.Group.GID).ToHashSet(StringComparer.Ordinal);
|
|
||||||
|
|
||||||
if (updatedList != null)
|
if (updatedList != null)
|
||||||
{
|
{
|
||||||
var previousGid = GetSelectedGid();
|
var previousGid = GetSelectedGid();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
@@ -11,12 +11,8 @@ using LightlessSync.Services;
|
|||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.Utils;
|
using LightlessSync.Utils;
|
||||||
using LightlessSync.WebAPI;
|
using LightlessSync.WebAPI;
|
||||||
using Serilog;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Reflection.Emit;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace LightlessSync.UI;
|
namespace LightlessSync.UI;
|
||||||
|
|
||||||
@@ -33,13 +29,14 @@ public class TopTabMenu
|
|||||||
private bool _pairRequestsExpanded; // useless for now
|
private bool _pairRequestsExpanded; // useless for now
|
||||||
private int _lastRequestCount;
|
private int _lastRequestCount;
|
||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
|
private readonly NotificationService _lightlessNotificationService;
|
||||||
private string _filter = string.Empty;
|
private string _filter = string.Empty;
|
||||||
private int _globalControlCountdown = 0;
|
private int _globalControlCountdown = 0;
|
||||||
private float _pairRequestsHeight = 150f;
|
private float _pairRequestsHeight = 150f;
|
||||||
private string _pairToAdd = string.Empty;
|
private string _pairToAdd = string.Empty;
|
||||||
|
|
||||||
private SelectedTab _selectedTab = SelectedTab.None;
|
private SelectedTab _selectedTab = SelectedTab.None;
|
||||||
public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService)
|
public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService)
|
||||||
{
|
{
|
||||||
_lightlessMediator = lightlessMediator;
|
_lightlessMediator = lightlessMediator;
|
||||||
_apiController = apiController;
|
_apiController = apiController;
|
||||||
@@ -47,6 +44,7 @@ public class TopTabMenu
|
|||||||
_pairRequestService = pairRequestService;
|
_pairRequestService = pairRequestService;
|
||||||
_dalamudUtilService = dalamudUtilService;
|
_dalamudUtilService = dalamudUtilService;
|
||||||
_uiSharedService = uiSharedService;
|
_uiSharedService = uiSharedService;
|
||||||
|
_lightlessNotificationService = lightlessNotificationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum SelectedTab
|
private enum SelectedTab
|
||||||
@@ -198,19 +196,6 @@ public class TopTabMenu
|
|||||||
|
|
||||||
if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f);
|
if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f);
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
if (ImGui.Button("Add Test Pair Request"))
|
|
||||||
{
|
|
||||||
var fakeCid = Guid.NewGuid().ToString("N");
|
|
||||||
var display = _pairRequestService.RegisterIncomingRequest(fakeCid, "Debug pair request");
|
|
||||||
_lightlessMediator.Publish(new NotificationMessage(
|
|
||||||
"Pair request received (debug)",
|
|
||||||
display.Message,
|
|
||||||
NotificationType.Info,
|
|
||||||
TimeSpan.FromSeconds(5)));
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
DrawIncomingPairRequests(availableWidth);
|
DrawIncomingPairRequests(availableWidth);
|
||||||
|
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
@@ -401,7 +386,7 @@ public class TopTabMenu
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var myCidHash = (await _dalamudUtilService.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
|
var myCidHash = (await _dalamudUtilService.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
|
||||||
await _apiController.TryPairWithContentId(request.HashedCid, myCidHash).ConfigureAwait(false);
|
await _apiController.TryPairWithContentId(request.HashedCid).ConfigureAwait(false);
|
||||||
_pairRequestService.RemoveRequest(request.HashedCid);
|
_pairRequestService.RemoveRequest(request.HashedCid);
|
||||||
|
|
||||||
var display = string.IsNullOrEmpty(request.DisplayName) ? request.HashedCid : request.DisplayName;
|
var display = string.IsNullOrEmpty(request.DisplayName) ? request.HashedCid : request.DisplayName;
|
||||||
@@ -850,4 +835,4 @@ public class TopTabMenu
|
|||||||
ImGui.EndPopup();
|
ImGui.EndPopup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,21 +11,20 @@ namespace LightlessSync.UI
|
|||||||
{ "LightlessPurple", "#ad8af5" },
|
{ "LightlessPurple", "#ad8af5" },
|
||||||
{ "LightlessPurpleActive", "#be9eff" },
|
{ "LightlessPurpleActive", "#be9eff" },
|
||||||
{ "LightlessPurpleDefault", "#9375d1" },
|
{ "LightlessPurpleDefault", "#9375d1" },
|
||||||
|
|
||||||
{ "ButtonDefault", "#323232" },
|
{ "ButtonDefault", "#323232" },
|
||||||
{ "FullBlack", "#000000" },
|
{ "FullBlack", "#000000" },
|
||||||
|
|
||||||
{ "LightlessBlue", "#a6c2ff" },
|
{ "LightlessBlue", "#a6c2ff" },
|
||||||
{ "LightlessYellow", "#ffe97a" },
|
{ "LightlessYellow", "#ffe97a" },
|
||||||
{ "LightlessYellow2", "#cfbd63" },
|
|
||||||
{ "LightlessGreen", "#7cd68a" },
|
{ "LightlessGreen", "#7cd68a" },
|
||||||
|
{ "LightlessOrange", "#ffb366" },
|
||||||
{ "PairBlue", "#88a2db" },
|
{ "PairBlue", "#88a2db" },
|
||||||
{ "DimRed", "#d44444" },
|
{ "DimRed", "#d44444" },
|
||||||
|
|
||||||
{ "LightlessAdminText", "#ffd663" },
|
{ "LightlessAdminText", "#ffd663" },
|
||||||
{ "LightlessAdminGlow", "#b09343" },
|
{ "LightlessAdminGlow", "#b09343" },
|
||||||
{ "LightlessModeratorText", "#94ffda" },
|
{ "LightlessModeratorText", "#94ffda" },
|
||||||
{ "LightlessModeratorGlow", "#599c84" },
|
|
||||||
|
{ "Lightfinder", "#ad8af5" },
|
||||||
|
{ "LightfinderEdge", "#000000" },
|
||||||
};
|
};
|
||||||
|
|
||||||
private static LightlessConfigService? _configService;
|
private static LightlessConfigService? _configService;
|
||||||
|
|||||||
756
LightlessSync/UI/UpdateNotesUi.cs
Normal file
756
LightlessSync/UI/UpdateNotesUi.cs
Normal file
@@ -0,0 +1,756 @@
|
|||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using Dalamud.Utility;
|
||||||
|
using LightlessSync.LightlessConfiguration;
|
||||||
|
using LightlessSync.Services;
|
||||||
|
using LightlessSync.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
using YamlDotNet.Serialization.NamingConventions;
|
||||||
|
using Dalamud.Interface;
|
||||||
|
using LightlessSync.UI.Models;
|
||||||
|
|
||||||
|
namespace LightlessSync.UI;
|
||||||
|
|
||||||
|
// Inspiration taken from Brio and Character Select+ (goats)
|
||||||
|
public class UpdateNotesUi : WindowMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private readonly UiSharedService _uiShared;
|
||||||
|
private readonly LightlessConfigService _configService;
|
||||||
|
|
||||||
|
private ChangelogFile _changelog = new();
|
||||||
|
private CreditsFile _credits = new();
|
||||||
|
private bool _scrollToTop;
|
||||||
|
private int _selectedTab;
|
||||||
|
private bool _hasInitializedCollapsingHeaders;
|
||||||
|
|
||||||
|
private struct Particle
|
||||||
|
{
|
||||||
|
public Vector2 Position;
|
||||||
|
public Vector2 Velocity;
|
||||||
|
public float Life;
|
||||||
|
public float MaxLife;
|
||||||
|
public float Size;
|
||||||
|
public ParticleType Type;
|
||||||
|
public List<Vector2>? Trail;
|
||||||
|
public float Twinkle;
|
||||||
|
public float Depth;
|
||||||
|
public float Hue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum ParticleType
|
||||||
|
{
|
||||||
|
TwinklingStar,
|
||||||
|
ShootingStar
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly List<Particle> _particles = [];
|
||||||
|
private float _particleSpawnTimer;
|
||||||
|
private readonly Random _random = new();
|
||||||
|
|
||||||
|
private const float _headerHeight = 150f;
|
||||||
|
private const float _particleSpawnInterval = 0.2f;
|
||||||
|
private const int _maxParticles = 50;
|
||||||
|
private const int _maxTrailLength = 50;
|
||||||
|
private const float _edgeFadeDistance = 30f;
|
||||||
|
private const float _extendedParticleHeight = 40f;
|
||||||
|
|
||||||
|
public UpdateNotesUi(ILogger<UpdateNotesUi> logger,
|
||||||
|
LightlessMediator mediator,
|
||||||
|
UiSharedService uiShared,
|
||||||
|
LightlessConfigService configService,
|
||||||
|
PerformanceCollectorService performanceCollectorService)
|
||||||
|
: base(logger, mediator, "Lightless Sync — Update Notes", performanceCollectorService)
|
||||||
|
{
|
||||||
|
logger.LogInformation("UpdateNotesUi constructor called");
|
||||||
|
_uiShared = uiShared;
|
||||||
|
_configService = configService;
|
||||||
|
|
||||||
|
AllowClickthrough = false;
|
||||||
|
AllowPinning = false;
|
||||||
|
RespectCloseHotkey = true;
|
||||||
|
ShowCloseButton = true;
|
||||||
|
|
||||||
|
Flags = ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse |
|
||||||
|
ImGuiWindowFlags.NoTitleBar;
|
||||||
|
|
||||||
|
SizeConstraints = new WindowSizeConstraints()
|
||||||
|
{
|
||||||
|
MinimumSize = new Vector2(800, 700), MaximumSize = new Vector2(800, 700),
|
||||||
|
};
|
||||||
|
|
||||||
|
LoadEmbeddedResources();
|
||||||
|
logger.LogInformation("UpdateNotesUi constructor completed successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnOpen()
|
||||||
|
{
|
||||||
|
_scrollToTop = true;
|
||||||
|
_hasInitializedCollapsingHeaders = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void DrawInternal()
|
||||||
|
{
|
||||||
|
if (_uiShared.IsInGpose)
|
||||||
|
return;
|
||||||
|
|
||||||
|
DrawHeader();
|
||||||
|
ImGuiHelpers.ScaledDummy(6);
|
||||||
|
DrawTabs();
|
||||||
|
DrawCloseButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawHeader()
|
||||||
|
{
|
||||||
|
var windowPos = ImGui.GetWindowPos();
|
||||||
|
var windowPadding = ImGui.GetStyle().WindowPadding;
|
||||||
|
var headerWidth = (800f * ImGuiHelpers.GlobalScale) - (windowPadding.X * 2);
|
||||||
|
|
||||||
|
var headerStart = windowPos + new Vector2(windowPadding.X, windowPadding.Y);
|
||||||
|
var headerEnd = headerStart + new Vector2(headerWidth, _headerHeight);
|
||||||
|
|
||||||
|
var extendedParticleSize = new Vector2(headerWidth, _headerHeight + _extendedParticleHeight);
|
||||||
|
|
||||||
|
DrawGradientBackground(headerStart, headerEnd);
|
||||||
|
DrawHeaderText(headerStart);
|
||||||
|
DrawHeaderButtons(headerStart, headerWidth);
|
||||||
|
DrawBottomGradient(headerStart, headerEnd, headerWidth);
|
||||||
|
|
||||||
|
ImGui.SetCursorPosY(windowPadding.Y + _headerHeight + 5);
|
||||||
|
ImGui.SetCursorPosX(20);
|
||||||
|
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||||
|
{
|
||||||
|
ImGui.TextColored(UIColors.Get("LightlessGreen"), FontAwesomeIcon.Star.ToIconString());
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
|
||||||
|
ImGui.TextColored(UIColors.Get("LightlessGreen"), "What's New");
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(_changelog.Tagline))
|
||||||
|
{
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 10);
|
||||||
|
ImGui.TextColored(new Vector4(0.75f, 0.75f, 0.85f, 1.0f), _changelog.Tagline);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(_changelog.Subline))
|
||||||
|
{
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.TextColored(new Vector4(0.65f, 0.65f, 0.75f, 1.0f), $" – {_changelog.Subline}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiHelpers.ScaledDummy(3);
|
||||||
|
|
||||||
|
DrawParticleEffects(headerStart, extendedParticleSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawGradientBackground(Vector2 headerStart, Vector2 headerEnd)
|
||||||
|
{
|
||||||
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
|
|
||||||
|
var darkPurple = new Vector4(0.08f, 0.05f, 0.15f, 1.0f);
|
||||||
|
var deepPurple = new Vector4(0.12f, 0.08f, 0.20f, 1.0f);
|
||||||
|
|
||||||
|
drawList.AddRectFilledMultiColor(
|
||||||
|
headerStart,
|
||||||
|
headerEnd,
|
||||||
|
ImGui.GetColorU32(darkPurple),
|
||||||
|
ImGui.GetColorU32(darkPurple),
|
||||||
|
ImGui.GetColorU32(deepPurple),
|
||||||
|
ImGui.GetColorU32(deepPurple)
|
||||||
|
);
|
||||||
|
|
||||||
|
var random = new Random(42);
|
||||||
|
for (int i = 0; i < 50; i++)
|
||||||
|
{
|
||||||
|
var starPos = headerStart + new Vector2(
|
||||||
|
(float)random.NextDouble() * (headerEnd.X - headerStart.X),
|
||||||
|
(float)random.NextDouble() * (headerEnd.Y - headerStart.Y)
|
||||||
|
);
|
||||||
|
var brightness = 0.3f + (float)random.NextDouble() * 0.4f;
|
||||||
|
drawList.AddCircleFilled(starPos, 1f, ImGui.GetColorU32(new Vector4(1f, 1f, 1f, brightness)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawBottomGradient(Vector2 headerStart, Vector2 headerEnd, float width)
|
||||||
|
{
|
||||||
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
|
var gradientHeight = 60f;
|
||||||
|
|
||||||
|
for (int i = 0; i < gradientHeight; i++)
|
||||||
|
{
|
||||||
|
var progress = i / gradientHeight;
|
||||||
|
var smoothProgress = progress * progress;
|
||||||
|
var r = 0.12f + (0.0f - 0.12f) * smoothProgress;
|
||||||
|
var g = 0.08f + (0.0f - 0.08f) * smoothProgress;
|
||||||
|
var b = 0.20f + (0.0f - 0.20f) * smoothProgress;
|
||||||
|
var alpha = 1f - smoothProgress;
|
||||||
|
var gradientColor = new Vector4(r, g, b, alpha);
|
||||||
|
drawList.AddLine(
|
||||||
|
new Vector2(headerStart.X, headerEnd.Y + i),
|
||||||
|
new Vector2(headerStart.X + width, headerEnd.Y + i),
|
||||||
|
ImGui.GetColorU32(gradientColor),
|
||||||
|
1f
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawHeaderText(Vector2 headerStart)
|
||||||
|
{
|
||||||
|
var textX = 20f;
|
||||||
|
var textY = 30f;
|
||||||
|
|
||||||
|
ImGui.SetCursorScreenPos(headerStart + new Vector2(textX, textY));
|
||||||
|
|
||||||
|
using (_uiShared.UidFont.Push())
|
||||||
|
{
|
||||||
|
ImGui.TextColored(new Vector4(0.95f, 0.95f, 0.95f, 1.0f), "Lightless Sync");
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SetCursorScreenPos(headerStart + new Vector2(textX, textY + 45f));
|
||||||
|
ImGui.TextColored(UIColors.Get("LightlessBlue"), "Update Notes");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawHeaderButtons(Vector2 headerStart, float headerWidth)
|
||||||
|
{
|
||||||
|
var buttonSize = _uiShared.GetIconButtonSize(FontAwesomeIcon.Globe);
|
||||||
|
var spacing = 8f * ImGuiHelpers.GlobalScale;
|
||||||
|
var rightPadding = 15f * ImGuiHelpers.GlobalScale;
|
||||||
|
var topPadding = 15f * ImGuiHelpers.GlobalScale;
|
||||||
|
var buttonY = headerStart.Y + topPadding;
|
||||||
|
var gitButtonX = headerStart.X + headerWidth - rightPadding - buttonSize.X;
|
||||||
|
var discordButtonX = gitButtonX - buttonSize.X - spacing;
|
||||||
|
|
||||||
|
ImGui.SetCursorScreenPos(new Vector2(discordButtonX, buttonY));
|
||||||
|
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Button, new Vector4(0, 0, 0, 0)))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurple") with { W = 0.3f }))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessPurpleActive") with { W = 0.5f }))
|
||||||
|
{
|
||||||
|
if (_uiShared.IconButton(FontAwesomeIcon.Comments))
|
||||||
|
{
|
||||||
|
Util.OpenLink("https://discord.gg/dsbjcXMnhA");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("Join our Discord");
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SetCursorScreenPos(new Vector2(gitButtonX, buttonY));
|
||||||
|
if (_uiShared.IconButton(FontAwesomeIcon.Code))
|
||||||
|
{
|
||||||
|
Util.OpenLink("https://git.lightless-sync.org/Lightless-Sync");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("View on Git");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawParticleEffects(Vector2 bannerStart, Vector2 bannerSize)
|
||||||
|
{
|
||||||
|
var deltaTime = ImGui.GetIO().DeltaTime;
|
||||||
|
_particleSpawnTimer += deltaTime;
|
||||||
|
|
||||||
|
if (_particleSpawnTimer > _particleSpawnInterval && _particles.Count < _maxParticles)
|
||||||
|
{
|
||||||
|
SpawnParticle(bannerSize);
|
||||||
|
_particleSpawnTimer = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_random.NextDouble() < 0.003)
|
||||||
|
{
|
||||||
|
SpawnShootingStar(bannerSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
|
|
||||||
|
for (int i = _particles.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var particle = _particles[i];
|
||||||
|
|
||||||
|
var screenPos = bannerStart + particle.Position;
|
||||||
|
|
||||||
|
if (particle.Type == ParticleType.ShootingStar && particle.Trail != null)
|
||||||
|
{
|
||||||
|
particle.Trail.Insert(0, particle.Position);
|
||||||
|
if (particle.Trail.Count > _maxTrailLength)
|
||||||
|
particle.Trail.RemoveAt(particle.Trail.Count - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (particle.Type == ParticleType.TwinklingStar)
|
||||||
|
{
|
||||||
|
particle.Twinkle += 0.005f * particle.Depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
particle.Position += particle.Velocity * deltaTime;
|
||||||
|
particle.Life -= deltaTime;
|
||||||
|
|
||||||
|
var isOutOfBounds = particle.Position.X < -50 || particle.Position.X > bannerSize.X + 50 ||
|
||||||
|
particle.Position.Y < -50 || particle.Position.Y > bannerSize.Y + 50;
|
||||||
|
|
||||||
|
if (particle.Life <= 0 || (particle.Type != ParticleType.TwinklingStar && isOutOfBounds))
|
||||||
|
{
|
||||||
|
_particles.RemoveAt(i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (particle.Type == ParticleType.TwinklingStar)
|
||||||
|
{
|
||||||
|
if (particle.Position.X < 0 || particle.Position.X > bannerSize.X)
|
||||||
|
particle.Velocity = particle.Velocity with { X = -particle.Velocity.X };
|
||||||
|
if (particle.Position.Y < 0 || particle.Position.Y > bannerSize.Y)
|
||||||
|
particle.Velocity = particle.Velocity with { Y = -particle.Velocity.Y };
|
||||||
|
}
|
||||||
|
|
||||||
|
var fadeIn = Math.Min(1f, (particle.MaxLife - particle.Life) / 20f);
|
||||||
|
var fadeOut = Math.Min(1f, particle.Life / 20f);
|
||||||
|
var lifeFade = Math.Min(fadeIn, fadeOut);
|
||||||
|
|
||||||
|
var edgeFadeX = Math.Min(
|
||||||
|
Math.Min(1f, (particle.Position.X + _edgeFadeDistance) / _edgeFadeDistance),
|
||||||
|
Math.Min(1f, (bannerSize.X - particle.Position.X + _edgeFadeDistance) / _edgeFadeDistance)
|
||||||
|
);
|
||||||
|
var edgeFadeY = Math.Min(
|
||||||
|
Math.Min(1f, (particle.Position.Y + _edgeFadeDistance) / _edgeFadeDistance),
|
||||||
|
Math.Min(1f, (bannerSize.Y - particle.Position.Y + _edgeFadeDistance) / _edgeFadeDistance)
|
||||||
|
);
|
||||||
|
var edgeFade = Math.Min(edgeFadeX, edgeFadeY);
|
||||||
|
|
||||||
|
var baseAlpha = lifeFade * edgeFade;
|
||||||
|
var finalAlpha = particle.Type == ParticleType.TwinklingStar
|
||||||
|
? baseAlpha * (0.6f + 0.4f * MathF.Sin(particle.Twinkle))
|
||||||
|
: baseAlpha;
|
||||||
|
|
||||||
|
if (particle.Type == ParticleType.ShootingStar && particle.Trail != null && particle.Trail.Count > 1)
|
||||||
|
{
|
||||||
|
var cyanColor = new Vector4(0.4f, 0.8f, 1.0f, 1.0f);
|
||||||
|
|
||||||
|
for (int t = 1; t < particle.Trail.Count; t++)
|
||||||
|
{
|
||||||
|
var trailProgress = (float)t / particle.Trail.Count;
|
||||||
|
var trailAlpha = Math.Min(1f, (1f - trailProgress) * finalAlpha * 1.8f);
|
||||||
|
var trailWidth = (1f - trailProgress) * 3f + 1f;
|
||||||
|
|
||||||
|
var glowAlpha = trailAlpha * 0.4f;
|
||||||
|
drawList.AddLine(
|
||||||
|
bannerStart + particle.Trail[t - 1],
|
||||||
|
bannerStart + particle.Trail[t],
|
||||||
|
ImGui.GetColorU32(cyanColor with { W = glowAlpha }),
|
||||||
|
trailWidth + 4f
|
||||||
|
);
|
||||||
|
|
||||||
|
drawList.AddLine(
|
||||||
|
bannerStart + particle.Trail[t - 1],
|
||||||
|
bannerStart + particle.Trail[t],
|
||||||
|
ImGui.GetColorU32(cyanColor with { W = trailAlpha }),
|
||||||
|
trailWidth
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (particle.Type == ParticleType.TwinklingStar)
|
||||||
|
{
|
||||||
|
DrawTwinklingStar(drawList, screenPos, particle.Size, particle.Hue, finalAlpha, particle.Depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
_particles[i] = particle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawTwinklingStar(ImDrawListPtr drawList, Vector2 position, float size, float hue, float alpha,
|
||||||
|
float depth)
|
||||||
|
{
|
||||||
|
var color = HslToRgb(hue, 1.0f, 0.85f);
|
||||||
|
color.W = alpha;
|
||||||
|
|
||||||
|
drawList.AddCircleFilled(position, size, ImGui.GetColorU32(color));
|
||||||
|
|
||||||
|
var glowColor = color with { W = alpha * 0.3f };
|
||||||
|
drawList.AddCircleFilled(position, size * (1.2f + depth * 0.3f), ImGui.GetColorU32(glowColor));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector4 HslToRgb(float h, float s, float l)
|
||||||
|
{
|
||||||
|
h = h / 360f;
|
||||||
|
float c = (1 - MathF.Abs(2 * l - 1)) * s;
|
||||||
|
float x = c * (1 - MathF.Abs((h * 6) % 2 - 1));
|
||||||
|
float m = l - c / 2;
|
||||||
|
|
||||||
|
float r, g, b;
|
||||||
|
if (h < 1f / 6f)
|
||||||
|
{
|
||||||
|
r = c;
|
||||||
|
g = x;
|
||||||
|
b = 0;
|
||||||
|
}
|
||||||
|
else if (h < 2f / 6f)
|
||||||
|
{
|
||||||
|
r = x;
|
||||||
|
g = c;
|
||||||
|
b = 0;
|
||||||
|
}
|
||||||
|
else if (h < 3f / 6f)
|
||||||
|
{
|
||||||
|
r = 0;
|
||||||
|
g = c;
|
||||||
|
b = x;
|
||||||
|
}
|
||||||
|
else if (h < 4f / 6f)
|
||||||
|
{
|
||||||
|
r = 0;
|
||||||
|
g = x;
|
||||||
|
b = c;
|
||||||
|
}
|
||||||
|
else if (h < 5f / 6f)
|
||||||
|
{
|
||||||
|
r = x;
|
||||||
|
g = 0;
|
||||||
|
b = c;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
r = c;
|
||||||
|
g = 0;
|
||||||
|
b = x;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Vector4(r + m, g + m, b + m, 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void SpawnParticle(Vector2 bannerSize)
|
||||||
|
{
|
||||||
|
var position = new Vector2(
|
||||||
|
(float)_random.NextDouble() * bannerSize.X,
|
||||||
|
(float)_random.NextDouble() * bannerSize.Y
|
||||||
|
);
|
||||||
|
|
||||||
|
var depthLayers = new[] { 0.5f, 1.0f, 1.5f };
|
||||||
|
var depth = depthLayers[_random.Next(depthLayers.Length)];
|
||||||
|
|
||||||
|
var velocity = new Vector2(
|
||||||
|
((float)_random.NextDouble() - 0.5f) * 0.05f * depth,
|
||||||
|
((float)_random.NextDouble() - 0.5f) * 0.05f * depth
|
||||||
|
);
|
||||||
|
|
||||||
|
var isBlue = _random.NextDouble() < 0.5;
|
||||||
|
var hue = isBlue ? 220f + (float)_random.NextDouble() * 30f : 270f + (float)_random.NextDouble() * 40f;
|
||||||
|
var size = (0.5f + (float)_random.NextDouble() * 2f) * depth;
|
||||||
|
var maxLife = 120f + (float)_random.NextDouble() * 60f;
|
||||||
|
|
||||||
|
_particles.Add(new Particle
|
||||||
|
{
|
||||||
|
Position = position,
|
||||||
|
Velocity = velocity,
|
||||||
|
Life = maxLife,
|
||||||
|
MaxLife = maxLife,
|
||||||
|
Size = size,
|
||||||
|
Type = ParticleType.TwinklingStar,
|
||||||
|
Trail = null,
|
||||||
|
Twinkle = (float)_random.NextDouble() * MathF.PI * 2,
|
||||||
|
Depth = depth,
|
||||||
|
Hue = hue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SpawnShootingStar(Vector2 bannerSize)
|
||||||
|
{
|
||||||
|
var maxLife = 80f + (float)_random.NextDouble() * 40f;
|
||||||
|
var startX = bannerSize.X * (0.3f + (float)_random.NextDouble() * 0.6f);
|
||||||
|
var startY = -10f;
|
||||||
|
|
||||||
|
_particles.Add(new Particle
|
||||||
|
{
|
||||||
|
Position = new Vector2(startX, startY),
|
||||||
|
Velocity = new Vector2(
|
||||||
|
-50f - (float)_random.NextDouble() * 40f,
|
||||||
|
30f + (float)_random.NextDouble() * 40f
|
||||||
|
),
|
||||||
|
Life = maxLife,
|
||||||
|
MaxLife = maxLife,
|
||||||
|
Size = 2.5f,
|
||||||
|
Type = ParticleType.ShootingStar,
|
||||||
|
Trail = new List<Vector2>(),
|
||||||
|
Twinkle = 0,
|
||||||
|
Depth = 1.0f,
|
||||||
|
Hue = 270f
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void DrawTabs()
|
||||||
|
{
|
||||||
|
using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 6f))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Tab, UIColors.Get("ButtonDefault")))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.TabHovered, UIColors.Get("LightlessPurple")))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.TabActive, UIColors.Get("LightlessPurpleActive")))
|
||||||
|
{
|
||||||
|
using (var tabBar = ImRaii.TabBar("###ll_tabs", ImGuiTabBarFlags.None))
|
||||||
|
{
|
||||||
|
if (!tabBar)
|
||||||
|
return;
|
||||||
|
|
||||||
|
using (var changelogTab = ImRaii.TabItem("Changelog"))
|
||||||
|
{
|
||||||
|
if (changelogTab)
|
||||||
|
{
|
||||||
|
_selectedTab = 0;
|
||||||
|
DrawChangelog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_credits.Credits != null && _credits.Credits.Count > 0)
|
||||||
|
{
|
||||||
|
using (var creditsTab = ImRaii.TabItem("Credits"))
|
||||||
|
{
|
||||||
|
if (creditsTab)
|
||||||
|
{
|
||||||
|
_selectedTab = 1;
|
||||||
|
DrawCredits();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawCredits()
|
||||||
|
{
|
||||||
|
using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 6f))
|
||||||
|
using (var child = ImRaii.Child("###ll_credits", new Vector2(0, ImGui.GetContentRegionAvail().Y - 60), false,
|
||||||
|
ImGuiWindowFlags.AlwaysVerticalScrollbar))
|
||||||
|
{
|
||||||
|
if (!child)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ImGui.PushTextWrapPos();
|
||||||
|
|
||||||
|
if (_credits.Credits != null)
|
||||||
|
{
|
||||||
|
foreach (var category in _credits.Credits)
|
||||||
|
{
|
||||||
|
DrawCreditCategory(category);
|
||||||
|
ImGuiHelpers.ScaledDummy(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.PopTextWrapPos();
|
||||||
|
ImGui.Spacing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawCreditCategory(CreditCategory category)
|
||||||
|
{
|
||||||
|
DrawFeatureSection(category.Category, UIColors.Get("LightlessBlue"));
|
||||||
|
|
||||||
|
foreach (var item in category.Items)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(item.Role))
|
||||||
|
{
|
||||||
|
ImGui.BulletText($"{item.Name} — {item.Role}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.BulletText(item.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawCloseButton()
|
||||||
|
{
|
||||||
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
|
|
||||||
|
var closeWidth = 200f * ImGuiHelpers.GlobalScale;
|
||||||
|
var closeHeight = 35f * ImGuiHelpers.GlobalScale;
|
||||||
|
ImGui.SetCursorPosX((ImGui.GetWindowSize().X - closeWidth) / 2);
|
||||||
|
|
||||||
|
using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 8f))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Button, UIColors.Get("LightlessPurple")))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurpleActive")))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.ButtonActive, UIColors.Get("ButtonDefault")))
|
||||||
|
{
|
||||||
|
if (ImGui.Button("Got it!", new Vector2(closeWidth, closeHeight)))
|
||||||
|
{
|
||||||
|
// Update last seen version when user acknowledges the update notes
|
||||||
|
var ver = Assembly.GetExecutingAssembly().GetName().Version;
|
||||||
|
var currentVersion = ver == null ? string.Empty : $"{ver.Major}.{ver.Minor}.{ver.Build}";
|
||||||
|
_configService.Current.LastSeenVersion = currentVersion;
|
||||||
|
_configService.Save();
|
||||||
|
|
||||||
|
IsOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("You can view this window again in the settings (title menu)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawChangelog()
|
||||||
|
{
|
||||||
|
using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 6f))
|
||||||
|
using (var child = ImRaii.Child("###ll_changelog", new Vector2(0, ImGui.GetContentRegionAvail().Y - 60), false,
|
||||||
|
ImGuiWindowFlags.AlwaysVerticalScrollbar))
|
||||||
|
{
|
||||||
|
if (!child)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_scrollToTop)
|
||||||
|
{
|
||||||
|
_scrollToTop = false;
|
||||||
|
ImGui.SetScrollHereY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.PushTextWrapPos();
|
||||||
|
|
||||||
|
foreach (var entry in _changelog.Changelog)
|
||||||
|
{
|
||||||
|
DrawChangelogEntry(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
_hasInitializedCollapsingHeaders = true;
|
||||||
|
|
||||||
|
ImGui.PopTextWrapPos();
|
||||||
|
ImGui.Spacing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawChangelogEntry(ChangelogEntry entry)
|
||||||
|
{
|
||||||
|
var isCurrent = entry.IsCurrent ?? false;
|
||||||
|
|
||||||
|
var currentColor = isCurrent
|
||||||
|
? UIColors.Get("LightlessGreen")
|
||||||
|
: new Vector4(0.95f, 0.95f, 1.0f, 1.0f);
|
||||||
|
|
||||||
|
var flags = isCurrent ? ImGuiTreeNodeFlags.DefaultOpen : ImGuiTreeNodeFlags.None;
|
||||||
|
|
||||||
|
if (!_hasInitializedCollapsingHeaders)
|
||||||
|
{
|
||||||
|
ImGui.SetNextItemOpen(isCurrent, ImGuiCond.Always);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isOpen;
|
||||||
|
using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 4f))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Header, UIColors.Get("ButtonDefault")))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, currentColor))
|
||||||
|
{
|
||||||
|
isOpen = ImGui.CollapsingHeader($" {entry.Name} — {entry.Date} ", flags);
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.TextColored(new Vector4(0.85f, 0.85f, 0.95f, 1.0f), $" — {entry.Tagline}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isOpen)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ImGuiHelpers.ScaledDummy(8);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(entry.Message))
|
||||||
|
{
|
||||||
|
ImGui.TextWrapped(entry.Message);
|
||||||
|
ImGuiHelpers.ScaledDummy(8);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.Versions != null)
|
||||||
|
{
|
||||||
|
foreach (var version in entry.Versions)
|
||||||
|
{
|
||||||
|
DrawFeatureSection(version.Number, UIColors.Get("LightlessGreen"));
|
||||||
|
|
||||||
|
foreach (var item in version.Items)
|
||||||
|
{
|
||||||
|
ImGui.BulletText(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawFeatureSection(string title, Vector4 accentColor)
|
||||||
|
{
|
||||||
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
|
var startPos = ImGui.GetCursorScreenPos();
|
||||||
|
var availableWidth = ImGui.GetContentRegionAvail().X;
|
||||||
|
|
||||||
|
var backgroundMin = startPos + new Vector2(-8, -4);
|
||||||
|
var backgroundMax = startPos + new Vector2(availableWidth + 8, 28);
|
||||||
|
|
||||||
|
var bgColor = new Vector4(0.12f, 0.12f, 0.15f, 0.7f);
|
||||||
|
drawList.AddRectFilled(backgroundMin, backgroundMax, ImGui.GetColorU32(bgColor), 6f);
|
||||||
|
|
||||||
|
drawList.AddRectFilled(
|
||||||
|
backgroundMin,
|
||||||
|
backgroundMin + new Vector2(4, backgroundMax.Y - backgroundMin.Y),
|
||||||
|
ImGui.GetColorU32(accentColor),
|
||||||
|
3f
|
||||||
|
);
|
||||||
|
|
||||||
|
var glowColor = accentColor with { W = 0.15f };
|
||||||
|
drawList.AddRect(
|
||||||
|
backgroundMin,
|
||||||
|
backgroundMax,
|
||||||
|
ImGui.GetColorU32(glowColor),
|
||||||
|
6f,
|
||||||
|
ImDrawFlags.None,
|
||||||
|
1.5f
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate vertical centering
|
||||||
|
var textSize = ImGui.CalcTextSize(title);
|
||||||
|
var boxHeight = backgroundMax.Y - backgroundMin.Y;
|
||||||
|
var verticalOffset = (boxHeight - textSize.Y) / 5f;
|
||||||
|
|
||||||
|
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 8);
|
||||||
|
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + verticalOffset);
|
||||||
|
ImGui.TextColored(accentColor, title);
|
||||||
|
ImGui.SetCursorPosY(backgroundMax.Y - startPos.Y + ImGui.GetCursorPosY());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadEmbeddedResources()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
|
var deserializer = new DeserializerBuilder()
|
||||||
|
.WithNamingConvention(CamelCaseNamingConvention.Instance)
|
||||||
|
.IgnoreUnmatchedProperties()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Load changelog
|
||||||
|
using var changelogStream = assembly.GetManifestResourceStream("LightlessSync.Changelog.changelog.yaml");
|
||||||
|
if (changelogStream != null)
|
||||||
|
{
|
||||||
|
using var reader = new StreamReader(changelogStream, Encoding.UTF8, true, 128);
|
||||||
|
var yaml = reader.ReadToEnd();
|
||||||
|
_changelog = deserializer.Deserialize<ChangelogFile>(yaml) ?? new();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load credits
|
||||||
|
using var creditsStream = assembly.GetManifestResourceStream("LightlessSync.Changelog.credits.yaml");
|
||||||
|
if (creditsStream != null)
|
||||||
|
{
|
||||||
|
using var reader = new StreamReader(creditsStream, Encoding.UTF8, true, 128);
|
||||||
|
var yaml = reader.ReadToEnd();
|
||||||
|
_credits = deserializer.Deserialize<CreditsFile>(yaml) ?? new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore - window will gracefully render with defaults
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
using System.Security.Cryptography;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace LightlessSync.Utils;
|
namespace LightlessSync.Utils;
|
||||||
@@ -13,8 +16,9 @@ public static class Crypto
|
|||||||
|
|
||||||
public static string GetFileHash(this string filePath)
|
public static string GetFileHash(this string filePath)
|
||||||
{
|
{
|
||||||
using SHA1CryptoServiceProvider cryptoProvider = new();
|
using SHA1 sha1 = SHA1.Create();
|
||||||
return BitConverter.ToString(cryptoProvider.ComputeHash(File.ReadAllBytes(filePath))).Replace("-", "", StringComparison.Ordinal);
|
using FileStream stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
|
||||||
|
return BitConverter.ToString(sha1.ComputeHash(stream)).Replace("-", "", StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetHash256(this (string, ushort) playerToHash)
|
public static string GetHash256(this (string, ushort) playerToHash)
|
||||||
|
|||||||
@@ -5,12 +5,18 @@ using LightlessSync.API.Dto.Files;
|
|||||||
using LightlessSync.API.Routes;
|
using LightlessSync.API.Routes;
|
||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
using LightlessSync.PlayerData.Handlers;
|
using LightlessSync.PlayerData.Handlers;
|
||||||
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.WebAPI.Files.Models;
|
using LightlessSync.WebAPI.Files.Models;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using LightlessSync.LightlessConfiguration;
|
||||||
|
|
||||||
namespace LightlessSync.WebAPI.Files;
|
namespace LightlessSync.WebAPI.Files;
|
||||||
|
|
||||||
@@ -20,17 +26,27 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
private readonly FileCompactor _fileCompactor;
|
private readonly FileCompactor _fileCompactor;
|
||||||
private readonly FileCacheManager _fileDbManager;
|
private readonly FileCacheManager _fileDbManager;
|
||||||
private readonly FileTransferOrchestrator _orchestrator;
|
private readonly FileTransferOrchestrator _orchestrator;
|
||||||
|
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||||
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly ConcurrentDictionary<ThrottledStream, byte> _activeDownloadStreams;
|
private readonly ConcurrentDictionary<ThrottledStream, byte> _activeDownloadStreams;
|
||||||
|
private static readonly TimeSpan DownloadStallTimeout = TimeSpan.FromSeconds(30);
|
||||||
|
private volatile bool _disableDirectDownloads;
|
||||||
|
private int _consecutiveDirectDownloadFailures;
|
||||||
|
private bool _lastConfigDirectDownloadsState;
|
||||||
|
|
||||||
public FileDownloadManager(ILogger<FileDownloadManager> logger, LightlessMediator mediator,
|
public FileDownloadManager(ILogger<FileDownloadManager> logger, LightlessMediator mediator,
|
||||||
FileTransferOrchestrator orchestrator,
|
FileTransferOrchestrator orchestrator,
|
||||||
FileCacheManager fileCacheManager, FileCompactor fileCompactor) : base(logger, mediator)
|
FileCacheManager fileCacheManager, FileCompactor fileCompactor,
|
||||||
|
PairProcessingLimiter pairProcessingLimiter, LightlessConfigService configService) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
_downloadStatus = new Dictionary<string, FileDownloadStatus>(StringComparer.Ordinal);
|
_downloadStatus = new Dictionary<string, FileDownloadStatus>(StringComparer.Ordinal);
|
||||||
_orchestrator = orchestrator;
|
_orchestrator = orchestrator;
|
||||||
_fileDbManager = fileCacheManager;
|
_fileDbManager = fileCacheManager;
|
||||||
_fileCompactor = fileCompactor;
|
_fileCompactor = fileCompactor;
|
||||||
|
_pairProcessingLimiter = pairProcessingLimiter;
|
||||||
|
_configService = configService;
|
||||||
_activeDownloadStreams = new();
|
_activeDownloadStreams = new();
|
||||||
|
_lastConfigDirectDownloadsState = _configService.Current.EnableDirectDownloads;
|
||||||
|
|
||||||
Mediator.Subscribe<DownloadLimitChangedMessage>(this, (msg) =>
|
Mediator.Subscribe<DownloadLimitChangedMessage>(this, (msg) =>
|
||||||
{
|
{
|
||||||
@@ -50,6 +66,11 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
public bool IsDownloading => CurrentDownloads.Any();
|
public bool IsDownloading => CurrentDownloads.Any();
|
||||||
|
|
||||||
|
private bool ShouldUseDirectDownloads()
|
||||||
|
{
|
||||||
|
return _configService.Current.EnableDirectDownloads && !_disableDirectDownloads;
|
||||||
|
}
|
||||||
|
|
||||||
public static void MungeBuffer(Span<byte> buffer)
|
public static void MungeBuffer(Span<byte> buffer)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < buffer.Length; ++i)
|
for (int i = 0; i < buffer.Length; ++i)
|
||||||
@@ -156,39 +177,47 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
Logger.LogWarning("Download status missing for {group} when starting download", downloadGroup);
|
Logger.LogWarning("Download status missing for {group} when starting download", downloadGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var requestUrl = LightlessFiles.CacheGetFullPath(fileTransfer[0].DownloadUri, requestId);
|
||||||
|
|
||||||
|
await DownloadFileThrottled(requestUrl, tempPath, progress, MungeBuffer, ct, withToken: true).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private delegate void DownloadDataCallback(Span<byte> data);
|
||||||
|
|
||||||
|
private async Task DownloadFileThrottled(Uri requestUrl, string destinationFilename, IProgress<long> progress, DownloadDataCallback? callback, CancellationToken ct, bool withToken)
|
||||||
|
{
|
||||||
const int maxRetries = 3;
|
const int maxRetries = 3;
|
||||||
int retryCount = 0;
|
int retryCount = 0;
|
||||||
TimeSpan retryDelay = TimeSpan.FromSeconds(2);
|
TimeSpan retryDelay = TimeSpan.FromSeconds(2);
|
||||||
|
HttpResponseMessage? response = null;
|
||||||
HttpResponseMessage response = null!;
|
|
||||||
var requestUrl = LightlessFiles.CacheGetFullPath(fileTransfer[0].DownloadUri, requestId);
|
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Attempt {attempt} - Downloading {requestUrl} for request {id}", retryCount + 1, requestUrl, requestId);
|
Logger.LogDebug("Attempt {attempt} - Downloading {requestUrl}", retryCount + 1, requestUrl);
|
||||||
|
response = await _orchestrator.SendRequestAsync(HttpMethod.Get, requestUrl, ct, HttpCompletionOption.ResponseHeadersRead, withToken).ConfigureAwait(false);
|
||||||
response = await _orchestrator.SendRequestAsync(HttpMethod.Get, requestUrl, ct, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
catch (HttpRequestException ex) when (ex.InnerException is TimeoutException || ex.StatusCode == null)
|
catch (HttpRequestException ex) when (ex.InnerException is TimeoutException || ex.StatusCode == null)
|
||||||
{
|
{
|
||||||
|
response?.Dispose();
|
||||||
retryCount++;
|
retryCount++;
|
||||||
|
|
||||||
Logger.LogWarning(ex, "Timeout during download of {requestUrl}. Attempt {attempt} of {maxRetries}", requestUrl, retryCount, maxRetries);
|
Logger.LogWarning(ex, "Timeout during download of {requestUrl}. Attempt {attempt} of {maxRetries}", requestUrl, retryCount, maxRetries);
|
||||||
|
|
||||||
if (retryCount >= maxRetries || ct.IsCancellationRequested)
|
if (retryCount >= maxRetries || ct.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
Logger.LogError($"Max retries reached or cancelled. Failing download for {requestUrl}");
|
Logger.LogError("Max retries reached or cancelled. Failing download for {requestUrl}", requestUrl);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.Delay(retryDelay, ct).ConfigureAwait(false); // Wait before retrying
|
await Task.Delay(retryDelay, ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (HttpRequestException ex)
|
catch (HttpRequestException ex)
|
||||||
{
|
{
|
||||||
|
response?.Dispose();
|
||||||
Logger.LogWarning(ex, "Error during download of {requestUrl}, HttpStatusCode: {code}", requestUrl, ex.StatusCode);
|
Logger.LogWarning(ex, "Error during download of {requestUrl}, HttpStatusCode: {code}", requestUrl, ex.StatusCode);
|
||||||
|
|
||||||
if (ex.StatusCode is HttpStatusCode.NotFound or HttpStatusCode.Unauthorized)
|
if (ex.StatusCode is HttpStatusCode.NotFound or HttpStatusCode.Unauthorized)
|
||||||
@@ -196,42 +225,80 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
throw new InvalidDataException($"Http error {ex.StatusCode} (cancelled: {ct.IsCancellationRequested}): {requestUrl}", ex);
|
throw new InvalidDataException($"Http error {ex.StatusCode} (cancelled: {ct.IsCancellationRequested}): {requestUrl}", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ThrottledStream? stream = null;
|
ThrottledStream? stream = null;
|
||||||
FileStream? fileStream = null;
|
FileStream? fileStream = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
fileStream = File.Create(tempPath);
|
fileStream = File.Create(destinationFilename);
|
||||||
await using (fileStream.ConfigureAwait(false))
|
await using (fileStream.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
var bufferSize = response.Content.Headers.ContentLength > 1024 * 1024 ? 65536 : 8196;
|
var bufferSize = response!.Content.Headers.ContentLength > 1024 * 1024 ? 65536 : 8196;
|
||||||
var buffer = new byte[bufferSize];
|
var buffer = new byte[bufferSize];
|
||||||
|
|
||||||
var bytesRead = 0;
|
|
||||||
var limit = _orchestrator.DownloadLimitPerSlot();
|
var limit = _orchestrator.DownloadLimitPerSlot();
|
||||||
Logger.LogTrace("Starting Download of {id} with a speed limit of {limit} to {tempPath}", requestId, limit, tempPath);
|
Logger.LogTrace("Starting Download with a speed limit of {limit} to {destination}", limit, destinationFilename);
|
||||||
|
|
||||||
stream = new(await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false), limit);
|
stream = new(await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false), limit);
|
||||||
|
|
||||||
_activeDownloadStreams.TryAdd(stream, 0);
|
_activeDownloadStreams.TryAdd(stream, 0);
|
||||||
|
|
||||||
while ((bytesRead = await stream.ReadAsync(buffer, ct).ConfigureAwait(false)) > 0)
|
while (true)
|
||||||
{
|
{
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
int bytesRead;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var readTask = stream.ReadAsync(buffer.AsMemory(0, buffer.Length), ct).AsTask();
|
||||||
|
while (!readTask.IsCompleted)
|
||||||
|
{
|
||||||
|
var completedTask = await Task.WhenAny(readTask, Task.Delay(DownloadStallTimeout)).ConfigureAwait(false);
|
||||||
|
if (completedTask == readTask)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
MungeBuffer(buffer.AsSpan(0, bytesRead));
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var snapshot = _pairProcessingLimiter.GetSnapshot();
|
||||||
|
if (snapshot.Waiting > 0)
|
||||||
|
{
|
||||||
|
throw new TimeoutException($"No data received for {DownloadStallTimeout.TotalSeconds} seconds while downloading {requestUrl} (waiting: {snapshot.Waiting})");
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogTrace("Download stalled for {requestUrl} but no queued pairs, continuing to wait", requestUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
bytesRead = await readTask.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytesRead == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback?.Invoke(buffer.AsSpan(0, bytesRead));
|
||||||
|
|
||||||
await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead), ct).ConfigureAwait(false);
|
await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead), ct).ConfigureAwait(false);
|
||||||
|
|
||||||
progress.Report(bytesRead);
|
progress.Report(bytesRead);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.LogDebug("{requestUrl} downloaded to {tempPath}", requestUrl, tempPath);
|
Logger.LogDebug("{requestUrl} downloaded to {destination}", requestUrl, destinationFilename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (TimeoutException ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Detected stalled download for {requestUrl}, aborting transfer", requestUrl);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
throw;
|
throw;
|
||||||
@@ -240,18 +307,18 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
fileStream?.Close();
|
fileStream?.Close();
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(tempPath) && File.Exists(tempPath))
|
if (!string.IsNullOrEmpty(destinationFilename) && File.Exists(destinationFilename))
|
||||||
{
|
{
|
||||||
File.Delete(tempPath);
|
File.Delete(destinationFilename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// Ignore errors during cleanup
|
// ignore cleanup errors
|
||||||
}
|
}
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -260,6 +327,134 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
_activeDownloadStreams.TryRemove(stream, out _);
|
_activeDownloadStreams.TryRemove(stream, out _);
|
||||||
await stream.DisposeAsync().ConfigureAwait(false);
|
await stream.DisposeAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
response?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DecompressBlockFileAsync(string downloadStatusKey, string blockFilePath, List<FileReplacementData> fileReplacement, string downloadLabel)
|
||||||
|
{
|
||||||
|
if (_downloadStatus.TryGetValue(downloadStatusKey, out var status))
|
||||||
|
{
|
||||||
|
status.TransferredFiles = 1;
|
||||||
|
status.DownloadStatus = DownloadStatus.Decompressing;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileStream? fileBlockStream = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fileBlockStream = File.OpenRead(blockFilePath);
|
||||||
|
while (fileBlockStream.Position < fileBlockStream.Length)
|
||||||
|
{
|
||||||
|
(string fileHash, long fileLengthBytes) = ReadBlockFileHeader(fileBlockStream);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fileExtension = fileReplacement.First(f => string.Equals(f.Hash, fileHash, StringComparison.OrdinalIgnoreCase)).GamePaths[0].Split(".")[^1];
|
||||||
|
var filePath = _fileDbManager.GetCacheFilePath(fileHash, fileExtension);
|
||||||
|
Logger.LogDebug("{dlName}: Decompressing {file}:{le} => {dest}", downloadLabel, fileHash, fileLengthBytes, filePath);
|
||||||
|
|
||||||
|
byte[] compressedFileContent = new byte[fileLengthBytes];
|
||||||
|
var readBytes = await fileBlockStream.ReadAsync(compressedFileContent, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
if (readBytes != fileLengthBytes)
|
||||||
|
{
|
||||||
|
throw new EndOfStreamException();
|
||||||
|
}
|
||||||
|
MungeBuffer(compressedFileContent);
|
||||||
|
|
||||||
|
var decompressedFile = LZ4Wrapper.Unwrap(compressedFileContent);
|
||||||
|
await _fileCompactor.WriteAllBytesAsync(filePath, decompressedFile, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
|
PersistFileToStorage(fileHash, filePath);
|
||||||
|
}
|
||||||
|
catch (EndOfStreamException)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("{dlName}: Failure to extract file {fileHash}, stream ended prematurely", downloadLabel, fileHash);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(e, "{dlName}: Error during decompression", downloadLabel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (EndOfStreamException)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("{dlName}: Failure to extract file header data, stream ended", downloadLabel);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "{dlName}: Error during block file read", downloadLabel);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (fileBlockStream != null)
|
||||||
|
await fileBlockStream.DisposeAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PerformDirectDownloadFallbackAsync(DownloadFileTransfer directDownload, List<FileReplacementData> fileReplacement,
|
||||||
|
IProgress<long> progress, CancellationToken token, bool slotAlreadyAcquired)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(directDownload.DirectDownloadUrl))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Direct download fallback requested without a direct download URL.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var downloadKey = directDownload.DirectDownloadUrl!;
|
||||||
|
bool slotAcquiredHere = false;
|
||||||
|
string? blockFile = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!slotAlreadyAcquired)
|
||||||
|
{
|
||||||
|
if (_downloadStatus.TryGetValue(downloadKey, out var tracker))
|
||||||
|
{
|
||||||
|
tracker.DownloadStatus = DownloadStatus.WaitingForSlot;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _orchestrator.WaitForDownloadSlotAsync(token).ConfigureAwait(false);
|
||||||
|
slotAcquiredHere = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_downloadStatus.TryGetValue(downloadKey, out var queueTracker))
|
||||||
|
{
|
||||||
|
queueTracker.DownloadStatus = DownloadStatus.WaitingForQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestIdResponse = await _orchestrator.SendRequestAsync(HttpMethod.Post, LightlessFiles.RequestEnqueueFullPath(directDownload.DownloadUri),
|
||||||
|
new[] { directDownload.Hash }, token).ConfigureAwait(false);
|
||||||
|
var requestId = Guid.Parse((await requestIdResponse.Content.ReadAsStringAsync().ConfigureAwait(false)).Trim('"'));
|
||||||
|
|
||||||
|
blockFile = _fileDbManager.GetCacheFilePath(requestId.ToString("N"), "blk");
|
||||||
|
|
||||||
|
await DownloadAndMungeFileHttpClient(downloadKey, requestId, [directDownload], blockFile, progress, token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!File.Exists(blockFile))
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException("Block file missing after direct download fallback.", blockFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
await DecompressBlockFileAsync(downloadKey, blockFile, fileReplacement, $"fallback-{directDownload.Hash}").ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (slotAcquiredHere)
|
||||||
|
{
|
||||||
|
_orchestrator.ReleaseDownloadSlot();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(blockFile))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(blockFile);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore cleanup errors
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,30 +502,76 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
private async Task DownloadFilesInternal(GameObjectHandler gameObjectHandler, List<FileReplacementData> fileReplacement, CancellationToken ct)
|
private async Task DownloadFilesInternal(GameObjectHandler gameObjectHandler, List<FileReplacementData> fileReplacement, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var downloadGroups = CurrentDownloads.GroupBy(f => f.DownloadUri.Host + ":" + f.DownloadUri.Port, StringComparer.Ordinal);
|
var objectName = gameObjectHandler?.Name ?? "Unknown";
|
||||||
|
|
||||||
foreach (var downloadGroup in downloadGroups)
|
var configAllowsDirect = _configService.Current.EnableDirectDownloads;
|
||||||
|
if (configAllowsDirect != _lastConfigDirectDownloadsState)
|
||||||
{
|
{
|
||||||
_downloadStatus[downloadGroup.Key] = new FileDownloadStatus()
|
_lastConfigDirectDownloadsState = configAllowsDirect;
|
||||||
|
if (configAllowsDirect)
|
||||||
|
{
|
||||||
|
_disableDirectDownloads = false;
|
||||||
|
_consecutiveDirectDownloadFailures = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowDirectDownloads = ShouldUseDirectDownloads();
|
||||||
|
|
||||||
|
var directDownloads = new List<DownloadFileTransfer>();
|
||||||
|
var batchDownloads = new List<DownloadFileTransfer>();
|
||||||
|
|
||||||
|
foreach (var download in CurrentDownloads)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(download.DirectDownloadUrl) && allowDirectDownloads)
|
||||||
|
{
|
||||||
|
directDownloads.Add(download);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
batchDownloads.Add(download);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var downloadBatches = batchDownloads.GroupBy(f => f.DownloadUri.Host + ":" + f.DownloadUri.Port, StringComparer.Ordinal).ToArray();
|
||||||
|
|
||||||
|
foreach (var directDownload in directDownloads)
|
||||||
|
{
|
||||||
|
_downloadStatus[directDownload.DirectDownloadUrl!] = new FileDownloadStatus()
|
||||||
{
|
{
|
||||||
DownloadStatus = DownloadStatus.Initializing,
|
DownloadStatus = DownloadStatus.Initializing,
|
||||||
TotalBytes = downloadGroup.Sum(c => c.Total),
|
TotalBytes = directDownload.Total,
|
||||||
TotalFiles = 1,
|
TotalFiles = 1,
|
||||||
TransferredBytes = 0,
|
TransferredBytes = 0,
|
||||||
TransferredFiles = 0
|
TransferredFiles = 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var downloadBatch in downloadBatches)
|
||||||
|
{
|
||||||
|
_downloadStatus[downloadBatch.Key] = new FileDownloadStatus()
|
||||||
|
{
|
||||||
|
DownloadStatus = DownloadStatus.Initializing,
|
||||||
|
TotalBytes = downloadBatch.Sum(c => c.Total),
|
||||||
|
TotalFiles = 1,
|
||||||
|
TransferredBytes = 0,
|
||||||
|
TransferredFiles = 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directDownloads.Count > 0 || downloadBatches.Length > 0)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Downloading {direct} files directly, and {batchtotal} in {batches} batches.", directDownloads.Count, batchDownloads.Count, downloadBatches.Length);
|
||||||
|
}
|
||||||
|
|
||||||
Mediator.Publish(new DownloadStartedMessage(gameObjectHandler, _downloadStatus));
|
Mediator.Publish(new DownloadStartedMessage(gameObjectHandler, _downloadStatus));
|
||||||
|
|
||||||
await Parallel.ForEachAsync(downloadGroups, new ParallelOptions()
|
Task batchDownloadsTask = downloadBatches.Length == 0 ? Task.CompletedTask : Parallel.ForEachAsync(downloadBatches, new ParallelOptions()
|
||||||
{
|
{
|
||||||
MaxDegreeOfParallelism = downloadGroups.Count(),
|
MaxDegreeOfParallelism = downloadBatches.Length,
|
||||||
CancellationToken = ct,
|
CancellationToken = ct,
|
||||||
},
|
},
|
||||||
async (fileGroup, token) =>
|
async (fileGroup, token) =>
|
||||||
{
|
{
|
||||||
// let server predownload files
|
|
||||||
var requestIdResponse = await _orchestrator.SendRequestAsync(HttpMethod.Post, LightlessFiles.RequestEnqueueFullPath(fileGroup.First().DownloadUri),
|
var requestIdResponse = await _orchestrator.SendRequestAsync(HttpMethod.Post, LightlessFiles.RequestEnqueueFullPath(fileGroup.First().DownloadUri),
|
||||||
fileGroup.Select(c => c.Hash), token).ConfigureAwait(false);
|
fileGroup.Select(c => c.Hash), token).ConfigureAwait(false);
|
||||||
Logger.LogDebug("Sent request for {n} files on server {uri} with result {result}", fileGroup.Count(), fileGroup.First().DownloadUri,
|
Logger.LogDebug("Sent request for {n} files on server {uri} with result {result}", fileGroup.Count(), fileGroup.First().DownloadUri,
|
||||||
@@ -353,7 +594,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
downloadStatus.DownloadStatus = DownloadStatus.WaitingForSlot;
|
downloadStatus.DownloadStatus = DownloadStatus.WaitingForSlot;
|
||||||
await _orchestrator.WaitForDownloadSlotAsync(token).ConfigureAwait(false);
|
await _orchestrator.WaitForDownloadSlotAsync(token).ConfigureAwait(false);
|
||||||
downloadStatus.DownloadStatus = DownloadStatus.WaitingForQueue;
|
downloadStatus.DownloadStatus = DownloadStatus.WaitingForQueue;
|
||||||
Progress<long> progress = new((bytesDownloaded) =>
|
var progress = CreateInlineProgress((bytesDownloaded) =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -371,7 +612,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("{dlName}: Detected cancellation of download, partially extracting files for {id}", fi.Name, gameObjectHandler);
|
Logger.LogDebug("{dlName}: Detected cancellation of download, partially extracting files for {id}", fi.Name, objectName);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -382,72 +623,167 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
FileStream? fileBlockStream = null;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_downloadStatus.TryGetValue(fileGroup.Key, out var status))
|
|
||||||
{
|
|
||||||
status.TransferredFiles = 1;
|
|
||||||
status.DownloadStatus = DownloadStatus.Decompressing;
|
|
||||||
}
|
|
||||||
if (!File.Exists(blockFile))
|
if (!File.Exists(blockFile))
|
||||||
{
|
{
|
||||||
Logger.LogWarning("{dlName}: Block file missing before extraction, skipping", fi.Name);
|
Logger.LogWarning("{dlName}: Block file missing before extraction, skipping", fi.Name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fileBlockStream = File.OpenRead(blockFile);
|
await DecompressBlockFileAsync(fileGroup.Key, blockFile, fileReplacement, fi.Name).ConfigureAwait(false);
|
||||||
while (fileBlockStream.Position < fileBlockStream.Length)
|
|
||||||
{
|
|
||||||
(string fileHash, long fileLengthBytes) = ReadBlockFileHeader(fileBlockStream);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var fileExtension = fileReplacement.First(f => string.Equals(f.Hash, fileHash, StringComparison.OrdinalIgnoreCase)).GamePaths[0].Split(".")[^1];
|
|
||||||
var filePath = _fileDbManager.GetCacheFilePath(fileHash, fileExtension);
|
|
||||||
Logger.LogDebug("{dlName}: Decompressing {file}:{le} => {dest}", fi.Name, fileHash, fileLengthBytes, filePath);
|
|
||||||
|
|
||||||
byte[] compressedFileContent = new byte[fileLengthBytes];
|
|
||||||
var readBytes = await fileBlockStream.ReadAsync(compressedFileContent, CancellationToken.None).ConfigureAwait(false);
|
|
||||||
if (readBytes != fileLengthBytes)
|
|
||||||
{
|
|
||||||
throw new EndOfStreamException();
|
|
||||||
}
|
|
||||||
MungeBuffer(compressedFileContent);
|
|
||||||
|
|
||||||
var decompressedFile = LZ4Wrapper.Unwrap(compressedFileContent);
|
|
||||||
await _fileCompactor.WriteAllBytesAsync(filePath, decompressedFile, CancellationToken.None).ConfigureAwait(false);
|
|
||||||
|
|
||||||
PersistFileToStorage(fileHash, filePath);
|
|
||||||
}
|
|
||||||
catch (EndOfStreamException)
|
|
||||||
{
|
|
||||||
Logger.LogWarning("{dlName}: Failure to extract file {fileHash}, stream ended prematurely", fi.Name, fileHash);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Logger.LogWarning(e, "{dlName}: Error during decompression", fi.Name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (EndOfStreamException)
|
|
||||||
{
|
|
||||||
Logger.LogDebug("{dlName}: Failure to extract file header data, stream ended", fi.Name);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "{dlName}: Error during block file read", fi.Name);
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_orchestrator.ReleaseDownloadSlot();
|
_orchestrator.ReleaseDownloadSlot();
|
||||||
if (fileBlockStream != null)
|
|
||||||
await fileBlockStream.DisposeAsync().ConfigureAwait(false);
|
|
||||||
File.Delete(blockFile);
|
File.Delete(blockFile);
|
||||||
}
|
}
|
||||||
}).ConfigureAwait(false);
|
});
|
||||||
|
|
||||||
Logger.LogDebug("Download end: {id}", gameObjectHandler);
|
Task directDownloadsTask = directDownloads.Count == 0 ? Task.CompletedTask : Parallel.ForEachAsync(directDownloads, new ParallelOptions()
|
||||||
|
{
|
||||||
|
MaxDegreeOfParallelism = directDownloads.Count,
|
||||||
|
CancellationToken = ct,
|
||||||
|
},
|
||||||
|
async (directDownload, token) =>
|
||||||
|
{
|
||||||
|
if (!_downloadStatus.TryGetValue(directDownload.DirectDownloadUrl!, out var downloadTracker))
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Download status missing for direct URL {url}", directDownload.DirectDownloadUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var progress = CreateInlineProgress((bytesDownloaded) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_downloadStatus.TryGetValue(directDownload.DirectDownloadUrl!, out FileDownloadStatus? value))
|
||||||
|
{
|
||||||
|
value.TransferredBytes += bytesDownloaded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Could not set download progress");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!ShouldUseDirectDownloads())
|
||||||
|
{
|
||||||
|
await PerformDirectDownloadFallbackAsync(directDownload, fileReplacement, progress, token, slotAlreadyAcquired: false).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tempFilename = _fileDbManager.GetCacheFilePath(directDownload.Hash, "bin");
|
||||||
|
var slotAcquired = false;
|
||||||
|
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
downloadTracker.DownloadStatus = DownloadStatus.WaitingForSlot;
|
||||||
|
await _orchestrator.WaitForDownloadSlotAsync(token).ConfigureAwait(false);
|
||||||
|
slotAcquired = true;
|
||||||
|
|
||||||
|
downloadTracker.DownloadStatus = DownloadStatus.Downloading;
|
||||||
|
Logger.LogDebug("Beginning direct download of {hash} from {url}", directDownload.Hash, directDownload.DirectDownloadUrl);
|
||||||
|
await DownloadFileThrottled(new Uri(directDownload.DirectDownloadUrl!), tempFilename, progress, null, token, withToken: false).ConfigureAwait(false);
|
||||||
|
|
||||||
|
Interlocked.Exchange(ref _consecutiveDirectDownloadFailures, 0);
|
||||||
|
|
||||||
|
downloadTracker.DownloadStatus = DownloadStatus.Decompressing;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var replacement = fileReplacement.FirstOrDefault(f => string.Equals(f.Hash, directDownload.Hash, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (replacement == null || replacement.GamePaths.Length == 0)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("{hash}: No replacement data found for direct download.", directDownload.Hash);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileExtension = replacement.GamePaths[0].Split(".")[^1];
|
||||||
|
var finalFilename = _fileDbManager.GetCacheFilePath(directDownload.Hash, fileExtension);
|
||||||
|
Logger.LogDebug("Decompressing direct download {hash} from {compressedFile} to {finalFile}", directDownload.Hash, tempFilename, finalFilename);
|
||||||
|
byte[] compressedBytes = await File.ReadAllBytesAsync(tempFilename).ConfigureAwait(false);
|
||||||
|
var decompressedBytes = LZ4Wrapper.Unwrap(compressedBytes);
|
||||||
|
await _fileCompactor.WriteAllBytesAsync(finalFilename, decompressedBytes, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
PersistFileToStorage(directDownload.Hash, finalFilename);
|
||||||
|
|
||||||
|
downloadTracker.TransferredFiles = 1;
|
||||||
|
Logger.LogDebug("Finished direct download of {hash}.", directDownload.Hash);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Exception downloading {hash} from {url}", directDownload.Hash, directDownload.DirectDownloadUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException ex)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("{hash}: Detected cancellation of direct download, discarding file.", directDownload.Hash);
|
||||||
|
Logger.LogError(ex, "{hash}: Error during direct download.", directDownload.Hash);
|
||||||
|
ClearDownload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var expectedDirectDownloadFailure = ex is InvalidDataException;
|
||||||
|
var failureCount = 0;
|
||||||
|
|
||||||
|
if (expectedDirectDownloadFailure)
|
||||||
|
{
|
||||||
|
Logger.LogInformation(ex, "{hash}: Direct download unavailable, attempting queued fallback.", directDownload.Hash);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
failureCount = Interlocked.Increment(ref _consecutiveDirectDownloadFailures);
|
||||||
|
Logger.LogWarning(ex, "{hash}: Direct download failed, attempting queued fallback.", directDownload.Hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
downloadTracker.DownloadStatus = DownloadStatus.WaitingForQueue;
|
||||||
|
await PerformDirectDownloadFallbackAsync(directDownload, fileReplacement, progress, token, slotAcquired).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!expectedDirectDownloadFailure && failureCount >= 3 && !_disableDirectDownloads)
|
||||||
|
{
|
||||||
|
_disableDirectDownloads = true;
|
||||||
|
Logger.LogWarning("Disabling direct downloads for this session after {count} consecutive failures.", failureCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception fallbackEx)
|
||||||
|
{
|
||||||
|
if (slotAcquired)
|
||||||
|
{
|
||||||
|
_orchestrator.ReleaseDownloadSlot();
|
||||||
|
slotAcquired = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogError(fallbackEx, "{hash}: Error during direct download fallback.", directDownload.Hash);
|
||||||
|
ClearDownload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (slotAcquired)
|
||||||
|
{
|
||||||
|
_orchestrator.ReleaseDownloadSlot();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(tempFilename);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(batchDownloadsTask, directDownloadsTask).ConfigureAwait(false);
|
||||||
|
|
||||||
|
Logger.LogDebug("Download end: {id}", objectName);
|
||||||
|
|
||||||
ClearDownload();
|
ClearDownload();
|
||||||
}
|
}
|
||||||
@@ -554,4 +890,24 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
_orchestrator.ClearDownloadRequest(requestId);
|
_orchestrator.ClearDownloadRequest(requestId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private static IProgress<long> CreateInlineProgress(Action<long> callback)
|
||||||
|
{
|
||||||
|
return new InlineProgress(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class InlineProgress : IProgress<long>
|
||||||
|
{
|
||||||
|
private readonly Action<long> _callback;
|
||||||
|
|
||||||
|
public InlineProgress(Action<long> callback)
|
||||||
|
{
|
||||||
|
_callback = callback ?? throw new ArgumentNullException(nameof(callback));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Report(long value)
|
||||||
|
{
|
||||||
|
_callback(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -81,27 +81,30 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<HttpResponseMessage> SendRequestAsync(HttpMethod method, Uri uri,
|
public async Task<HttpResponseMessage> SendRequestAsync(HttpMethod method, Uri uri,
|
||||||
CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
|
CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead,
|
||||||
|
bool withToken = true)
|
||||||
{
|
{
|
||||||
using var requestMessage = new HttpRequestMessage(method, uri);
|
using var requestMessage = new HttpRequestMessage(method, uri);
|
||||||
return await SendRequestInternalAsync(requestMessage, ct, httpCompletionOption).ConfigureAwait(false);
|
return await SendRequestInternalAsync(requestMessage, ct, httpCompletionOption, withToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<HttpResponseMessage> SendRequestAsync<T>(HttpMethod method, Uri uri, T content, CancellationToken ct) where T : class
|
public async Task<HttpResponseMessage> SendRequestAsync<T>(HttpMethod method, Uri uri, T content, CancellationToken ct,
|
||||||
|
bool withToken = true) where T : class
|
||||||
{
|
{
|
||||||
using var requestMessage = new HttpRequestMessage(method, uri);
|
using var requestMessage = new HttpRequestMessage(method, uri);
|
||||||
if (content is not ByteArrayContent)
|
if (content is not ByteArrayContent)
|
||||||
requestMessage.Content = JsonContent.Create(content);
|
requestMessage.Content = JsonContent.Create(content);
|
||||||
else
|
else
|
||||||
requestMessage.Content = content as ByteArrayContent;
|
requestMessage.Content = content as ByteArrayContent;
|
||||||
return await SendRequestInternalAsync(requestMessage, ct).ConfigureAwait(false);
|
return await SendRequestInternalAsync(requestMessage, ct, withToken: withToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<HttpResponseMessage> SendRequestStreamAsync(HttpMethod method, Uri uri, ProgressableStreamContent content, CancellationToken ct)
|
public async Task<HttpResponseMessage> SendRequestStreamAsync(HttpMethod method, Uri uri, ProgressableStreamContent content,
|
||||||
|
CancellationToken ct, bool withToken = true)
|
||||||
{
|
{
|
||||||
using var requestMessage = new HttpRequestMessage(method, uri);
|
using var requestMessage = new HttpRequestMessage(method, uri);
|
||||||
requestMessage.Content = content;
|
requestMessage.Content = content;
|
||||||
return await SendRequestInternalAsync(requestMessage, ct).ConfigureAwait(false);
|
return await SendRequestInternalAsync(requestMessage, ct, withToken: withToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task WaitForDownloadSlotAsync(CancellationToken token)
|
public async Task WaitForDownloadSlotAsync(CancellationToken token)
|
||||||
@@ -144,10 +147,13 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task<HttpResponseMessage> SendRequestInternalAsync(HttpRequestMessage requestMessage,
|
private async Task<HttpResponseMessage> SendRequestInternalAsync(HttpRequestMessage requestMessage,
|
||||||
CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
|
CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead, bool withToken = true)
|
||||||
{
|
{
|
||||||
var token = await _tokenProvider.GetToken().ConfigureAwait(false);
|
if (withToken)
|
||||||
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
{
|
||||||
|
var token = await _tokenProvider.GetToken().ConfigureAwait(false);
|
||||||
|
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||||
|
}
|
||||||
|
|
||||||
if (requestMessage.Content != null && requestMessage.Content is not StreamContent && requestMessage.Content is not ByteArrayContent)
|
if (requestMessage.Content != null && requestMessage.Content is not StreamContent && requestMessage.Content is not ByteArrayContent)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public class DownloadFileTransfer : FileTransfer
|
|||||||
}
|
}
|
||||||
get => Dto.Size;
|
get => Dto.Size;
|
||||||
}
|
}
|
||||||
|
public string? DirectDownloadUrl => ((DownloadFileDto)TransferDto).CDNDownloadUrl;
|
||||||
|
|
||||||
public long TotalRaw => Dto.RawSize;
|
public long TotalRaw => Dto.RawSize;
|
||||||
private DownloadFileDto Dto => (DownloadFileDto)TransferDto;
|
private DownloadFileDto Dto => (DownloadFileDto)TransferDto;
|
||||||
|
|||||||
@@ -35,16 +35,16 @@ public partial class ApiController
|
|||||||
await _lightlessHub!.SendAsync(nameof(UserAddPair), user).ConfigureAwait(false);
|
await _lightlessHub!.SendAsync(nameof(UserAddPair), user).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task TryPairWithContentId(string otherCid, string myCid)
|
public async Task TryPairWithContentId(string otherCid)
|
||||||
{
|
{
|
||||||
if (!IsConnected) return;
|
if (!IsConnected) return;
|
||||||
await _lightlessHub!.SendAsync(nameof(TryPairWithContentId), otherCid, myCid).ConfigureAwait(false);
|
await _lightlessHub!.SendAsync(nameof(TryPairWithContentId), otherCid).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetBroadcastStatus(string hashedCid, bool enabled, GroupBroadcastRequestDto? groupDto = null)
|
public async Task SetBroadcastStatus(bool enabled, GroupBroadcastRequestDto? groupDto = null)
|
||||||
{
|
{
|
||||||
CheckConnection();
|
CheckConnection();
|
||||||
await _lightlessHub!.InvokeAsync(nameof(SetBroadcastStatus), hashedCid, enabled, groupDto).ConfigureAwait(false);
|
await _lightlessHub!.InvokeAsync(nameof(SetBroadcastStatus), enabled, groupDto).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<BroadcastStatusInfoDto?> IsUserBroadcasting(string hashedCid)
|
public async Task<BroadcastStatusInfoDto?> IsUserBroadcasting(string hashedCid)
|
||||||
@@ -59,10 +59,10 @@ public partial class ApiController
|
|||||||
return await _lightlessHub!.InvokeAsync<BroadcastStatusBatchDto>(nameof(AreUsersBroadcasting), hashedCids).ConfigureAwait(false);
|
return await _lightlessHub!.InvokeAsync<BroadcastStatusBatchDto>(nameof(AreUsersBroadcasting), hashedCids).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TimeSpan?> GetBroadcastTtl(string hashedCid)
|
public async Task<TimeSpan?> GetBroadcastTtl()
|
||||||
{
|
{
|
||||||
CheckConnection();
|
CheckConnection();
|
||||||
return await _lightlessHub!.InvokeAsync<TimeSpan?>(nameof(GetBroadcastTtl), hashedCid).ConfigureAwait(false);
|
return await _lightlessHub!.InvokeAsync<TimeSpan?>(nameof(GetBroadcastTtl)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UserDelete()
|
public async Task UserDelete()
|
||||||
@@ -84,7 +84,7 @@ public partial class ApiController
|
|||||||
|
|
||||||
public async Task<UserProfileDto> UserGetProfile(UserDto dto)
|
public async Task<UserProfileDto> UserGetProfile(UserDto dto)
|
||||||
{
|
{
|
||||||
if (!IsConnected) return new UserProfileDto(dto.User, Disabled: false, IsNSFW: null, ProfilePictureBase64: null, Description: null);
|
if (!IsConnected) return new UserProfileDto(dto.User, Disabled: false, IsNSFW: null, ProfilePictureBase64: null, Description: null, Tags: null);
|
||||||
return await _lightlessHub!.InvokeAsync<UserProfileDto>(nameof(UserGetProfile), dto).ConfigureAwait(false);
|
return await _lightlessHub!.InvokeAsync<UserProfileDto>(nameof(UserGetProfile), dto).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.API.Data.Enum;
|
using LightlessSync.API.Data.Enum;
|
||||||
using LightlessSync.API.Dto;
|
using LightlessSync.API.Dto;
|
||||||
using LightlessSync.API.Dto.CharaData;
|
using LightlessSync.API.Dto.CharaData;
|
||||||
@@ -6,6 +6,7 @@ using LightlessSync.API.Dto.Group;
|
|||||||
using LightlessSync.API.Dto.User;
|
using LightlessSync.API.Dto.User;
|
||||||
using LightlessSync.LightlessConfiguration.Models;
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
|
using LightlessSync.Utils;
|
||||||
using Microsoft.AspNetCore.SignalR.Client;
|
using Microsoft.AspNetCore.SignalR.Client;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@@ -104,25 +105,27 @@ 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)
|
Logger.LogDebug("Client_ReceiveBroadcastPairRequest: {dto}", dto);
|
||||||
|
|
||||||
|
if (dto is null)
|
||||||
|
{
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
var request = _pairRequestService.RegisterIncomingRequest(dto.myHashedCid, dto.message ?? string.Empty);
|
ExecuteSafely(() =>
|
||||||
|
{
|
||||||
Mediator.Publish(new NotificationMessage(
|
Mediator.Publish(new PairRequestReceivedMessage(dto.myHashedCid, dto.message ?? string.Empty));
|
||||||
"Pair request received",
|
});
|
||||||
request.Message,
|
|
||||||
NotificationType.Info,
|
|
||||||
TimeSpan.FromSeconds(5)));
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Client_UpdateSystemInfo(SystemInfoDto systemInfo)
|
public Task Client_UpdateSystemInfo(SystemInfoDto systemInfo)
|
||||||
{
|
{
|
||||||
SystemInfoDto = systemInfo;
|
SystemInfoDto = systemInfo;
|
||||||
|
//Mediator.Publish(new UpdateSystemInfoMessage(systemInfo));
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +195,14 @@ public partial class ApiController
|
|||||||
public Task Client_UserUpdateProfile(UserDto dto)
|
public Task Client_UserUpdateProfile(UserDto dto)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Client_UserUpdateProfile: {dto}", dto);
|
Logger.LogDebug("Client_UserUpdateProfile: {dto}", dto);
|
||||||
ExecuteSafely(() => Mediator.Publish(new ClearProfileDataMessage(dto.User)));
|
ExecuteSafely(() => Mediator.Publish(new ClearProfileUserDataMessage(dto.User)));
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Client_GroupSendProfile(GroupProfileDto groupInfo)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Client_GroupSendProfile: {dto}", groupInfo);
|
||||||
|
ExecuteSafely(() => Mediator.Publish(new ClearProfileGroupDataMessage(groupInfo.Group)));
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,6 +387,12 @@ public partial class ApiController
|
|||||||
_lightlessHub!.On(nameof(Client_UserUpdateProfile), act);
|
_lightlessHub!.On(nameof(Client_UserUpdateProfile), act);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ClientGroupSendProfile(Action<GroupProfileDto> act)
|
||||||
|
{
|
||||||
|
if (_initialized) return;
|
||||||
|
_lightlessHub!.On(nameof(Client_GroupSendProfile), act);
|
||||||
|
}
|
||||||
|
|
||||||
public void OnUserUpdateSelfPairPermissions(Action<UserPermissionsDto> act)
|
public void OnUserUpdateSelfPairPermissions(Action<UserPermissionsDto> act)
|
||||||
{
|
{
|
||||||
if (_initialized) return;
|
if (_initialized) return;
|
||||||
|
|||||||
@@ -115,6 +115,18 @@ public partial class ApiController
|
|||||||
CheckConnection();
|
CheckConnection();
|
||||||
return await _lightlessHub!.InvokeAsync<int>(nameof(GroupPrune), group, days, execute).ConfigureAwait(false);
|
return await _lightlessHub!.InvokeAsync<int>(nameof(GroupPrune), group, days, execute).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
public async Task<GroupProfileDto> GroupGetProfile(GroupDto dto)
|
||||||
|
{
|
||||||
|
CheckConnection();
|
||||||
|
if (!IsConnected) return new GroupProfileDto(Group: dto.Group, Description: null, Tags: null, PictureBase64: null, IsNsfw: false, IsDisabled: false);
|
||||||
|
return await _lightlessHub!.InvokeAsync<GroupProfileDto>(nameof(GroupGetProfile), dto).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task GroupSetProfile(GroupProfileDto dto)
|
||||||
|
{
|
||||||
|
CheckConnection();
|
||||||
|
await _lightlessHub!.InvokeAsync(nameof(GroupSetProfile), dto).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<List<GroupFullInfoDto>> GroupsGetAll()
|
public async Task<List<GroupFullInfoDto>> GroupsGetAll()
|
||||||
{
|
{
|
||||||
@@ -139,7 +151,6 @@ public partial class ApiController
|
|||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void CheckConnection()
|
private void CheckConnection()
|
||||||
{
|
{
|
||||||
if (ServerState is not (ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting)) throw new InvalidDataException("Not connected");
|
if (ServerState is not (ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting)) throw new InvalidDataException("Not connected");
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -44,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) : base(logger, mediator)
|
TokenProvider tokenProvider, LightlessConfigService lightlessConfigService, NotificationService lightlessNotificationService) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
_hubFactory = hubFactory;
|
_hubFactory = hubFactory;
|
||||||
_dalamudUtil = dalamudUtil;
|
_dalamudUtil = dalamudUtil;
|
||||||
@@ -606,17 +606,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
ServerState = state;
|
ServerState = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Client_GroupSendProfile(GroupProfileDto groupInfo)
|
public Task<UserProfileDto?> UserGetLightfinderProfile(string hashedCid)
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<GroupProfileDto> GroupGetProfile(GroupDto dto)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task GroupSetProfile(GroupProfileDto dto)
|
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,6 +133,12 @@
|
|||||||
"Microsoft.IdentityModel.Tokens": "8.7.0"
|
"Microsoft.IdentityModel.Tokens": "8.7.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"YamlDotNet": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[16.3.0, )",
|
||||||
|
"resolved": "16.3.0",
|
||||||
|
"contentHash": "SgMOdxbz8X65z8hraIs6hOEdnkH6hESTAIUa7viEngHOYaH+6q5XJmwr1+yb9vJpNQ19hCQY69xbFsLtXpobQA=="
|
||||||
|
},
|
||||||
"K4os.Compression.LZ4": {
|
"K4os.Compression.LZ4": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "1.3.8",
|
"resolved": "1.3.8",
|
||||||
|
|||||||
Submodule PenumbraAPI updated: dd14131793...648b6fc2ce
Reference in New Issue
Block a user