Compare commits
2 Commits
1.12.2.9-D
...
notificati
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9030549a7 | ||
|
|
10336a829a |
Submodule LightlessAPI updated: bb92cd477d...44fbe10458
@@ -1,179 +0,0 @@
|
|||||||
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."
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
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"
|
|
||||||
@@ -27,7 +27,6 @@ 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)
|
||||||
@@ -463,7 +462,6 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
string[] existingLines = File.ReadAllLines(_csvPath);
|
string[] existingLines = File.ReadAllLines(_csvPath);
|
||||||
if (existingLines.Length > 0 && TryParseVersionHeader(existingLines[0], out var existingVersion) && existingVersion == FileCacheVersion)
|
if (existingLines.Length > 0 && TryParseVersionHeader(existingLines[0], out var existingVersion) && existingVersion == FileCacheVersion)
|
||||||
{
|
{
|
||||||
_csvHeaderEnsured = true;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,18 +481,6 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
}
|
}
|
||||||
|
|
||||||
File.WriteAllText(_csvPath, rebuilt.ToString());
|
File.WriteAllText(_csvPath, rebuilt.ToString());
|
||||||
_csvHeaderEnsured = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EnsureCsvHeaderLockedCached()
|
|
||||||
{
|
|
||||||
if (_csvHeaderEnsured)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
EnsureCsvHeaderLocked();
|
|
||||||
_csvHeaderEnsured = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BackupUnsupportedCache(string suffix)
|
private void BackupUnsupportedCache(string suffix)
|
||||||
@@ -554,11 +540,10 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
if (!File.Exists(_csvPath))
|
if (!File.Exists(_csvPath))
|
||||||
{
|
{
|
||||||
File.WriteAllLines(_csvPath, new[] { BuildVersionHeader(), entity.CsvEntry });
|
File.WriteAllLines(_csvPath, new[] { BuildVersionHeader(), entity.CsvEntry });
|
||||||
_csvHeaderEnsured = true;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
EnsureCsvHeaderLockedCached();
|
EnsureCsvHeaderLocked();
|
||||||
File.AppendAllLines(_csvPath, new[] { entity.CsvEntry });
|
File.AppendAllLines(_csvPath, new[] { entity.CsvEntry });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,33 +2,25 @@
|
|||||||
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 : IDisposable
|
public sealed class FileCompactor
|
||||||
{
|
{
|
||||||
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;
|
||||||
@@ -37,18 +29,6 @@ public sealed class FileCompactor : IDisposable
|
|||||||
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
|
||||||
@@ -107,30 +87,7 @@ public sealed class FileCompactor : IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
EnqueueCompaction(filePath);
|
CompactFile(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")]
|
||||||
@@ -269,67 +226,4 @@ public sealed class FileCompactor : IDisposable
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -67,7 +67,6 @@ public class LightlessConfig : ILightlessConfiguration
|
|||||||
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;
|
||||||
@@ -147,5 +146,4 @@ 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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ 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;
|
||||||
@@ -102,7 +101,7 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
|
|||||||
|
|
||||||
UIColors.Initialize(_lightlessConfigService);
|
UIColors.Initialize(_lightlessConfigService);
|
||||||
Mediator.StartQueueProcessing();
|
Mediator.StartQueueProcessing();
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,24 +115,6 @@ 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()
|
||||||
{
|
{
|
||||||
@@ -173,7 +154,6 @@ 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.2.9</Version>
|
<Version>1.12.3</Version>
|
||||||
<Description></Description>
|
<Description></Description>
|
||||||
<Copyright></Copyright>
|
<Copyright></Copyright>
|
||||||
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
||||||
@@ -46,7 +46,6 @@
|
|||||||
<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>
|
||||||
@@ -65,8 +64,6 @@
|
|||||||
<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,6 +1,4 @@
|
|||||||
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;
|
||||||
@@ -12,38 +10,21 @@ 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(
|
public FileDownloadManagerFactory(ILoggerFactory loggerFactory, LightlessMediator lightlessMediator, FileTransferOrchestrator fileTransferOrchestrator,
|
||||||
ILoggerFactory loggerFactory,
|
FileCacheManager fileCacheManager, FileCompactor fileCompactor)
|
||||||
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(
|
return new FileDownloadManager(_loggerFactory.CreateLogger<FileDownloadManager>(), _lightlessMediator, _fileTransferOrchestrator, _fileCacheManager, _fileCompactor);
|
||||||
_loggerFactory.CreateLogger<FileDownloadManager>(),
|
|
||||||
_lightlessMediator,
|
|
||||||
_fileTransferOrchestrator,
|
|
||||||
_fileCacheManager,
|
|
||||||
_fileCompactor,
|
|
||||||
_pairProcessingLimiter,
|
|
||||||
_configService);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 ClearProfileUserDataMessage(pair.UserData));
|
Mediator.Publish(new ClearProfileDataMessage(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 ClearProfileUserDataMessage(dto.User));
|
Mediator.Publish(new ClearProfileDataMessage(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 ClearProfileUserDataMessage(dto.User));
|
Mediator.Publish(new ClearProfileDataMessage(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 ClearProfileUserDataMessage(dto.User));
|
Mediator.Publish(new ClearProfileDataMessage(dto.User));
|
||||||
}
|
}
|
||||||
|
|
||||||
pair.UserPair.OwnPermissions = dto.Permissions;
|
pair.UserPair.OwnPermissions = dto.Permissions;
|
||||||
|
|||||||
@@ -190,7 +190,8 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
notificationManager,
|
notificationManager,
|
||||||
chatGui,
|
chatGui,
|
||||||
s.GetRequiredService<LightlessMediator>(),
|
s.GetRequiredService<LightlessMediator>(),
|
||||||
s.GetRequiredService<PairRequestService>()));
|
s.GetRequiredService<PairRequestService>(),
|
||||||
|
s.GetRequiredService<BroadcastService>()));
|
||||||
collection.AddSingleton((s) =>
|
collection.AddSingleton((s) =>
|
||||||
{
|
{
|
||||||
var httpClient = new HttpClient();
|
var httpClient = new HttpClient();
|
||||||
@@ -246,7 +247,6 @@ 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>(),
|
||||||
@@ -255,9 +255,9 @@ 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) =>
|
collection.AddScoped<WindowMediatorSubscriberBase, LightlessNotificationUI>((s) =>
|
||||||
new LightlessNotificationUi(
|
new LightlessNotificationUI(
|
||||||
s.GetRequiredService<ILogger<LightlessNotificationUi>>(),
|
s.GetRequiredService<ILogger<LightlessNotificationUI>>(),
|
||||||
s.GetRequiredService<LightlessMediator>(),
|
s.GetRequiredService<LightlessMediator>(),
|
||||||
s.GetRequiredService<PerformanceCollectorService>(),
|
s.GetRequiredService<PerformanceCollectorService>(),
|
||||||
s.GetRequiredService<LightlessConfigService>()));
|
s.GetRequiredService<LightlessConfigService>()));
|
||||||
@@ -269,7 +269,8 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
s.GetRequiredService<WindowSystem>(), s.GetServices<WindowMediatorSubscriberBase>(),
|
s.GetRequiredService<WindowSystem>(), s.GetServices<WindowMediatorSubscriberBase>(),
|
||||||
s.GetRequiredService<UiFactory>(),
|
s.GetRequiredService<UiFactory>(),
|
||||||
s.GetRequiredService<FileDialogManager>(),
|
s.GetRequiredService<FileDialogManager>(),
|
||||||
s.GetRequiredService<LightlessMediator>()));
|
s.GetRequiredService<LightlessMediator>(),
|
||||||
|
s.GetRequiredService<NotificationService>()));
|
||||||
collection.AddScoped((s) => new CommandManagerService(commandManager, s.GetRequiredService<PerformanceCollectorService>(),
|
collection.AddScoped((s) => new CommandManagerService(commandManager, s.GetRequiredService<PerformanceCollectorService>(),
|
||||||
s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<CacheMonitor>(), s.GetRequiredService<ApiController>(),
|
s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<CacheMonitor>(), s.GetRequiredService<ApiController>(),
|
||||||
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<LightlessConfigService>()));
|
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<LightlessConfigService>()));
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
using Dalamud.Interface;
|
|
||||||
using LightlessSync.LightlessConfiguration.Models;
|
|
||||||
using LightlessSync.UI;
|
|
||||||
using LightlessSync.UI.Models;
|
|
||||||
using LightlessSync.API.Dto.Group;
|
using LightlessSync.API.Dto.Group;
|
||||||
using LightlessSync.API.Dto.User;
|
using LightlessSync.API.Dto.User;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
@@ -144,11 +140,6 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
|||||||
IsLightFinderAvailable = false;
|
IsLightFinderAvailable = false;
|
||||||
ApplyBroadcastDisabled(forcePublish: true);
|
ApplyBroadcastDisabled(forcePublish: true);
|
||||||
_logger.LogDebug("Cleared Lightfinder state due to disconnect.");
|
_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)
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
@@ -245,11 +236,6 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
|||||||
{
|
{
|
||||||
_logger.LogInformation("Auto-enabling Lightfinder broadcast after reconnect.");
|
_logger.LogInformation("Auto-enabling Lightfinder broadcast after reconnect.");
|
||||||
_mediator.Publish(new EnableBroadcastMessage(hashedCid, true));
|
_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)
|
||||||
@@ -405,14 +391,13 @@ 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(
|
_mediator.Publish(new NotificationMessage(
|
||||||
"Broadcast Unavailable",
|
"Broadcast Unavailable",
|
||||||
"Lightfinder is not available on this server.",
|
"Lightfinder is not available on this server.",
|
||||||
NotificationType.Error));
|
LightlessConfiguration.Models.NotificationType.Error));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,7 +410,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
|||||||
_mediator.Publish(new NotificationMessage(
|
_mediator.Publish(new NotificationMessage(
|
||||||
"Broadcast Cooldown",
|
"Broadcast Cooldown",
|
||||||
$"Please wait {cd.TotalSeconds:F0} seconds before re-enabling broadcast.",
|
$"Please wait {cd.TotalSeconds:F0} seconds before re-enabling broadcast.",
|
||||||
NotificationType.Warning));
|
LightlessConfiguration.Models.NotificationType.Warning));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,15 +439,15 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
|||||||
_mediator.Publish(new NotificationMessage(
|
_mediator.Publish(new NotificationMessage(
|
||||||
newStatus ? "Broadcast Enabled" : "Broadcast Disabled",
|
newStatus ? "Broadcast Enabled" : "Broadcast Disabled",
|
||||||
newStatus ? "Your Lightfinder broadcast has been enabled." : "Your Lightfinder broadcast has been disabled.",
|
newStatus ? "Your Lightfinder broadcast has been enabled." : "Your Lightfinder broadcast has been disabled.",
|
||||||
NotificationType.Info));
|
LightlessConfiguration.Models.NotificationType.Info));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Failed to determine current broadcast status for toggle");
|
_logger.LogError(ex, "Failed to determine current broadcast status for toggle");
|
||||||
_mediator.Publish(new NotificationMessage(
|
_mediator.Publish(new NotificationMessage(
|
||||||
"Broadcast Toggle Failed",
|
"Broadcast Failed",
|
||||||
$"Failed to toggle broadcast: {ex.Message}",
|
$"Failed to toggle broadcast: {ex.Message}",
|
||||||
NotificationType.Error));
|
LightlessConfiguration.Models.NotificationType.Error));
|
||||||
}
|
}
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -525,7 +510,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
|||||||
{
|
{
|
||||||
_logger.LogDebug("Broadcast TTL expired. Disabling broadcast locally.");
|
_logger.LogDebug("Broadcast TTL expired. Disabling broadcast locally.");
|
||||||
ApplyBroadcastDisabled(forcePublish: true);
|
ApplyBroadcastDisabled(forcePublish: true);
|
||||||
ShowBroadcastExpiredNotification();
|
_mediator.Publish(new BroadcastExpiredMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -534,49 +519,4 @@ 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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -6,11 +6,7 @@ using LightlessSync.UI;
|
|||||||
using LightlessSync.Utils;
|
using LightlessSync.Utils;
|
||||||
using Lumina.Data.Files;
|
using Lumina.Data.Files;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.Immutable;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
namespace LightlessSync.Services;
|
namespace LightlessSync.Services;
|
||||||
|
|
||||||
public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||||
@@ -20,7 +16,6 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
|||||||
private CancellationTokenSource? _analysisCts;
|
private CancellationTokenSource? _analysisCts;
|
||||||
private CancellationTokenSource _baseAnalysisCts = new();
|
private CancellationTokenSource _baseAnalysisCts = new();
|
||||||
private string _lastDataHash = string.Empty;
|
private string _lastDataHash = string.Empty;
|
||||||
private CharacterAnalysisSummary _latestSummary = CharacterAnalysisSummary.Empty;
|
|
||||||
|
|
||||||
public CharacterAnalyzer(ILogger<CharacterAnalyzer> logger, LightlessMediator mediator, FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer)
|
public CharacterAnalyzer(ILogger<CharacterAnalyzer> logger, LightlessMediator mediator, FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer)
|
||||||
: base(logger, mediator)
|
: base(logger, mediator)
|
||||||
@@ -39,7 +34,6 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
|||||||
public bool IsAnalysisRunning => _analysisCts != null;
|
public bool IsAnalysisRunning => _analysisCts != null;
|
||||||
public int TotalFiles { get; internal set; }
|
public int TotalFiles { get; internal set; }
|
||||||
internal Dictionary<ObjectKind, Dictionary<string, FileDataEntry>> LastAnalysis { get; } = [];
|
internal Dictionary<ObjectKind, Dictionary<string, FileDataEntry>> LastAnalysis { get; } = [];
|
||||||
public CharacterAnalysisSummary LatestSummary => _latestSummary;
|
|
||||||
|
|
||||||
public void CancelAnalyze()
|
public void CancelAnalyze()
|
||||||
{
|
{
|
||||||
@@ -86,8 +80,6 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RecalculateSummary();
|
|
||||||
|
|
||||||
Mediator.Publish(new CharacterDataAnalyzedMessage());
|
Mediator.Publish(new CharacterDataAnalyzedMessage());
|
||||||
|
|
||||||
_analysisCts.CancelDispose();
|
_analysisCts.CancelDispose();
|
||||||
@@ -145,39 +137,11 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
|||||||
LastAnalysis[obj.Key] = data;
|
LastAnalysis[obj.Key] = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
RecalculateSummary();
|
|
||||||
|
|
||||||
Mediator.Publish(new CharacterDataAnalyzedMessage());
|
Mediator.Publish(new CharacterDataAnalyzedMessage());
|
||||||
|
|
||||||
_lastDataHash = charaData.DataHash.Value;
|
_lastDataHash = charaData.DataHash.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RecalculateSummary()
|
|
||||||
{
|
|
||||||
var builder = ImmutableDictionary.CreateBuilder<ObjectKind, CharacterAnalysisObjectSummary>();
|
|
||||||
|
|
||||||
foreach (var (objectKind, entries) in LastAnalysis)
|
|
||||||
{
|
|
||||||
long totalTriangles = 0;
|
|
||||||
long texOriginalBytes = 0;
|
|
||||||
long texCompressedBytes = 0;
|
|
||||||
|
|
||||||
foreach (var entry in entries.Values)
|
|
||||||
{
|
|
||||||
totalTriangles += entry.Triangles;
|
|
||||||
if (string.Equals(entry.FileType, "tex", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
texOriginalBytes += entry.OriginalSize;
|
|
||||||
texCompressedBytes += entry.CompressedSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
builder[objectKind] = new CharacterAnalysisObjectSummary(entries.Count, totalTriangles, texOriginalBytes, texCompressedBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
_latestSummary = new CharacterAnalysisSummary(builder.ToImmutable());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PrintAnalysis()
|
private void PrintAnalysis()
|
||||||
{
|
{
|
||||||
if (LastAnalysis.Count == 0) return;
|
if (LastAnalysis.Count == 0) return;
|
||||||
@@ -268,24 +232,4 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public readonly record struct CharacterAnalysisObjectSummary(int EntryCount, long TotalTriangles, long TexOriginalBytes, long TexCompressedBytes)
|
|
||||||
{
|
|
||||||
public bool HasEntries => EntryCount > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class CharacterAnalysisSummary
|
|
||||||
{
|
|
||||||
public static CharacterAnalysisSummary Empty { get; } =
|
|
||||||
new(ImmutableDictionary<ObjectKind, CharacterAnalysisObjectSummary>.Empty);
|
|
||||||
|
|
||||||
internal CharacterAnalysisSummary(IImmutableDictionary<ObjectKind, CharacterAnalysisObjectSummary> objects)
|
|
||||||
{
|
|
||||||
Objects = objects;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IImmutableDictionary<ObjectKind, CharacterAnalysisObjectSummary> Objects { get; }
|
|
||||||
|
|
||||||
public bool HasData => Objects.Any(kvp => kvp.Value.HasEntries);
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
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));
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace LightlessSync.Services;
|
namespace LightlessSync.Services;
|
||||||
|
|
||||||
public record LightlessUserProfileData(bool IsFlagged, bool IsNSFW, string Base64ProfilePicture, string Base64SupporterPicture, string Description)
|
public record LightlessProfileData(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));
|
||||||
File diff suppressed because one or more lines are too long
@@ -70,8 +70,7 @@ public record DownloadStartedMessage(GameObjectHandler DownloadId, Dictionary<st
|
|||||||
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 ClearProfileUserDataMessage(UserData? UserData = null) : MessageBase;
|
public record ClearProfileDataMessage(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;
|
||||||
@@ -108,9 +107,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 PairRequestReceivedMessage(string SenderName, string SenderId) : MessageBase;
|
||||||
|
public record BroadcastExpiredMessage : MessageBase;
|
||||||
public record VisibilityChange : MessageBase;
|
public record VisibilityChange : MessageBase;
|
||||||
#pragma warning restore S2094
|
#pragma warning restore S2094
|
||||||
#pragma warning restore MA0048 // File name must match type name
|
#pragma warning restore MA0048 // File name must match type name
|
||||||
@@ -208,13 +208,7 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
|||||||
|
|
||||||
for (int i = 0; i < ui3DModule->NamePlateObjectInfoCount; ++i)
|
for (int i = 0; i < ui3DModule->NamePlateObjectInfoCount; ++i)
|
||||||
{
|
{
|
||||||
if (ui3DModule->NamePlateObjectInfoPointers.IsEmpty) continue;
|
var objectInfo = ui3DModule->NamePlateObjectInfoPointers[i].Value;
|
||||||
|
|
||||||
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;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
private readonly INotificationManager _notificationManager;
|
private readonly INotificationManager _notificationManager;
|
||||||
private readonly IChatGui _chatGui;
|
private readonly IChatGui _chatGui;
|
||||||
private readonly PairRequestService _pairRequestService;
|
private readonly PairRequestService _pairRequestService;
|
||||||
|
private readonly BroadcastService _broadcastService;
|
||||||
private readonly HashSet<string> _shownPairRequestNotifications = new();
|
private readonly HashSet<string> _shownPairRequestNotifications = new();
|
||||||
|
|
||||||
public NotificationService(
|
public NotificationService(
|
||||||
@@ -32,7 +33,8 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
INotificationManager notificationManager,
|
INotificationManager notificationManager,
|
||||||
IChatGui chatGui,
|
IChatGui chatGui,
|
||||||
LightlessMediator mediator,
|
LightlessMediator mediator,
|
||||||
PairRequestService pairRequestService) : base(logger, mediator)
|
PairRequestService pairRequestService,
|
||||||
|
BroadcastService broadcastService) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
@@ -40,15 +42,16 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
_notificationManager = notificationManager;
|
_notificationManager = notificationManager;
|
||||||
_chatGui = chatGui;
|
_chatGui = chatGui;
|
||||||
_pairRequestService = pairRequestService;
|
_pairRequestService = pairRequestService;
|
||||||
|
_broadcastService = broadcastService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Mediator.Subscribe<NotificationMessage>(this, HandleNotificationMessage);
|
Mediator.Subscribe<NotificationMessage>(this, HandleNotificationMessage);
|
||||||
Mediator.Subscribe<PairRequestReceivedMessage>(this, HandlePairRequestReceived);
|
|
||||||
Mediator.Subscribe<PairRequestsUpdatedMessage>(this, HandlePairRequestsUpdated);
|
Mediator.Subscribe<PairRequestsUpdatedMessage>(this, HandlePairRequestsUpdated);
|
||||||
Mediator.Subscribe<PairDownloadStatusMessage>(this, HandlePairDownloadStatus);
|
Mediator.Subscribe<PairRequestReceivedMessage>(this, HandlePairRequestReceived);
|
||||||
Mediator.Subscribe<PerformanceNotificationMessage>(this, HandlePerformanceNotification);
|
Mediator.Subscribe<PerformanceNotificationMessage>(this, HandlePerformanceNotification);
|
||||||
|
Mediator.Subscribe<BroadcastExpiredMessage>(this, HandleBroadcastExpired);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,8 +298,33 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ShowPairDownloadNotification(List<(string playerName, float progress, string status)> downloadStatus,
|
||||||
|
int queueWaiting = 0)
|
||||||
|
{
|
||||||
|
var userDownloads = downloadStatus.Where(x => x.playerName != "Pair Queue").ToList();
|
||||||
|
var totalProgress = userDownloads.Count > 0 ? userDownloads.Average(x => x.progress) : 0f;
|
||||||
|
var message = BuildPairDownloadMessage(userDownloads, queueWaiting);
|
||||||
|
|
||||||
private string BuildPairDownloadMessage(List<(string PlayerName, float Progress, string Status)> userDownloads,
|
var notification = new LightlessNotification
|
||||||
|
{
|
||||||
|
Id = "pair_download_progress",
|
||||||
|
Title = "Downloading Pair Data",
|
||||||
|
Message = message,
|
||||||
|
Type = NotificationType.Download,
|
||||||
|
Duration = TimeSpan.FromSeconds(_configService.Current.DownloadNotificationDurationSeconds),
|
||||||
|
ShowProgress = true,
|
||||||
|
Progress = totalProgress
|
||||||
|
};
|
||||||
|
|
||||||
|
Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||||
|
|
||||||
|
if (AreAllDownloadsCompleted(userDownloads))
|
||||||
|
{
|
||||||
|
DismissPairDownloadNotification();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BuildPairDownloadMessage(List<(string playerName, float progress, string status)> userDownloads,
|
||||||
int queueWaiting)
|
int queueWaiting)
|
||||||
{
|
{
|
||||||
var messageParts = new List<string>();
|
var messageParts = new List<string>();
|
||||||
@@ -308,7 +336,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
|
|
||||||
if (userDownloads.Count > 0)
|
if (userDownloads.Count > 0)
|
||||||
{
|
{
|
||||||
var completedCount = userDownloads.Count(x => x.Progress >= 1.0f);
|
var completedCount = userDownloads.Count(x => x.progress >= 1.0f);
|
||||||
messageParts.Add($"Progress: {completedCount}/{userDownloads.Count} completed");
|
messageParts.Add($"Progress: {completedCount}/{userDownloads.Count} completed");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,29 +349,29 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
return string.Join("\n", messageParts);
|
return string.Join("\n", messageParts);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string BuildActiveDownloadLines(List<(string PlayerName, float Progress, string Status)> userDownloads)
|
private string BuildActiveDownloadLines(List<(string playerName, float progress, string status)> userDownloads)
|
||||||
{
|
{
|
||||||
var activeDownloads = userDownloads
|
var activeDownloads = userDownloads
|
||||||
.Where(x => x.Progress < 1.0f)
|
.Where(x => x.progress < 1.0f)
|
||||||
.Take(_configService.Current.MaxConcurrentPairApplications);
|
.Take(_configService.Current.MaxConcurrentPairApplications);
|
||||||
|
|
||||||
if (!activeDownloads.Any()) return string.Empty;
|
if (!activeDownloads.Any()) return string.Empty;
|
||||||
|
|
||||||
return string.Join("\n", activeDownloads.Select(x => $"• {x.PlayerName}: {FormatDownloadStatus(x)}"));
|
return string.Join("\n", activeDownloads.Select(x => $"• {x.playerName}: {FormatDownloadStatus(x)}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private string FormatDownloadStatus((string PlayerName, float Progress, string Status) download) =>
|
private string FormatDownloadStatus((string playerName, float progress, string status) download) =>
|
||||||
download.Status switch
|
download.status switch
|
||||||
{
|
{
|
||||||
"downloading" => $"{download.Progress:P0}",
|
"downloading" => $"{download.progress:P0}",
|
||||||
"decompressing" => "decompressing",
|
"decompressing" => "decompressing",
|
||||||
"queued" => "queued",
|
"queued" => "queued",
|
||||||
"waiting" => "waiting for slot",
|
"waiting" => "waiting for slot",
|
||||||
_ => download.Status
|
_ => download.status
|
||||||
};
|
};
|
||||||
|
|
||||||
private bool AreAllDownloadsCompleted(List<(string PlayerName, float Progress, string Status)> userDownloads) =>
|
private bool AreAllDownloadsCompleted(List<(string playerName, float progress, string status)> userDownloads) =>
|
||||||
userDownloads.Any() && userDownloads.All(x => x.Progress >= 1.0f);
|
userDownloads.Any() && userDownloads.All(x => x.progress >= 1.0f);
|
||||||
|
|
||||||
public void DismissPairDownloadNotification() =>
|
public void DismissPairDownloadNotification() =>
|
||||||
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
|
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
|
||||||
@@ -560,15 +588,12 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
|
|
||||||
private void HandlePairRequestReceived(PairRequestReceivedMessage msg)
|
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(
|
ShowPairRequestNotification(
|
||||||
senderName,
|
msg.SenderName,
|
||||||
request.HashedCid,
|
msg.SenderId,
|
||||||
onAccept: () => _pairRequestService.AcceptPairRequest(request.HashedCid, senderName),
|
() => _pairRequestService.AcceptPairRequest(msg.SenderId, msg.SenderName),
|
||||||
onDecline: () => _pairRequestService.DeclinePairRequest(request.HashedCid, senderName));
|
() => _pairRequestService.DeclinePairRequest(msg.SenderId)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandlePairRequestsUpdated(PairRequestsUpdatedMessage _)
|
private void HandlePairRequestsUpdated(PairRequestsUpdatedMessage _)
|
||||||
@@ -576,7 +601,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
var activeRequests = _pairRequestService.GetActiveRequests();
|
var activeRequests = _pairRequestService.GetActiveRequests();
|
||||||
var activeRequestIds = activeRequests.Select(r => r.HashedCid).ToHashSet();
|
var activeRequestIds = activeRequests.Select(r => r.HashedCid).ToHashSet();
|
||||||
|
|
||||||
// Dismiss notifications for requests that are no longer active (expired)
|
// Dismiss notifications for requests that are no longer active
|
||||||
var notificationsToRemove = _shownPairRequestNotifications
|
var notificationsToRemove = _shownPairRequestNotifications
|
||||||
.Where(hashedCid => !activeRequestIds.Contains(hashedCid))
|
.Where(hashedCid => !activeRequestIds.Contains(hashedCid))
|
||||||
.ToList();
|
.ToList();
|
||||||
@@ -587,30 +612,11 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
Mediator.Publish(new LightlessNotificationDismissMessage(notificationId));
|
Mediator.Publish(new LightlessNotificationDismissMessage(notificationId));
|
||||||
_shownPairRequestNotifications.Remove(hashedCid);
|
_shownPairRequestNotifications.Remove(hashedCid);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void HandlePairDownloadStatus(PairDownloadStatusMessage msg)
|
// Track active requests
|
||||||
{
|
foreach (var request in activeRequests)
|
||||||
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",
|
_shownPairRequestNotifications.Add(request.HashedCid);
|
||||||
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"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -753,4 +759,73 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
|||||||
}
|
}
|
||||||
return $"{playerName} ({userData.UID})";
|
return $"{playerName} ({userData.UID})";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void HandleBroadcastExpired(BroadcastExpiredMessage _)
|
||||||
|
{
|
||||||
|
var location = GetNotificationLocation(NotificationType.Warning);
|
||||||
|
|
||||||
|
if (location == NotificationLocation.Chat || location == NotificationLocation.ChatAndLightlessUi)
|
||||||
|
{
|
||||||
|
ShowChat(new NotificationMessage("Broadcast Expired", "Your Lightfinder broadcast has expired after 3 hours.", NotificationType.Warning));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((location == NotificationLocation.LightlessUi || location == NotificationLocation.ChatAndLightlessUi)
|
||||||
|
&& _configService.Current.UseLightlessNotifications)
|
||||||
|
{
|
||||||
|
var notification = new LightlessNotification
|
||||||
|
{
|
||||||
|
Id = "broadcast_expired",
|
||||||
|
Title = "Broadcast Expired",
|
||||||
|
Message = "Your Lightfinder broadcast has expired after 3 hours. Would you like to re-enable it?",
|
||||||
|
Type = NotificationType.Warning,
|
||||||
|
Duration = TimeSpan.FromSeconds(_configService.Current.WarningNotificationDurationSeconds),
|
||||||
|
SoundEffectId = GetSoundEffectId(NotificationType.Warning, null),
|
||||||
|
Actions = CreateBroadcastExpiredActions()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (notification.SoundEffectId.HasValue)
|
||||||
|
{
|
||||||
|
PlayNotificationSound(notification.SoundEffectId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||||
|
}
|
||||||
|
else if (location != NotificationLocation.Nowhere && location != NotificationLocation.Chat)
|
||||||
|
{
|
||||||
|
HandleNotificationMessage(new NotificationMessage("Broadcast Expired", "Your Lightfinder broadcast has expired after 3 hours.", NotificationType.Warning));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<LightlessNotificationAction> CreateBroadcastExpiredActions()
|
||||||
|
{
|
||||||
|
return new List<LightlessNotificationAction>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = "re_enable",
|
||||||
|
Label = "Re-enable Broadcast",
|
||||||
|
Icon = FontAwesomeIcon.Plus,
|
||||||
|
Color = UIColors.Get("LightlessGreen"),
|
||||||
|
IsPrimary = true,
|
||||||
|
OnClick = (n) =>
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Re-enabling broadcast from notification");
|
||||||
|
_broadcastService.ToggleBroadcast();
|
||||||
|
DismissNotification(n);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = "close",
|
||||||
|
Label = "Close",
|
||||||
|
Icon = FontAwesomeIcon.Times,
|
||||||
|
Color = UIColors.Get("DimRed"),
|
||||||
|
OnClick = (n) =>
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Broadcast expiration notification dismissed");
|
||||||
|
DismissNotification(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,6 @@ 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;
|
||||||
|
|
||||||
@@ -71,7 +70,7 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
if (!IsEnabled)
|
if (!IsEnabled)
|
||||||
{
|
{
|
||||||
TryReleaseSemaphore();
|
_semaphore.Release();
|
||||||
return NoopReleaser.Instance;
|
return NoopReleaser.Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,12 +90,18 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
|||||||
var releaseAmount = HardLimit - _semaphore.CurrentCount;
|
var releaseAmount = HardLimit - _semaphore.CurrentCount;
|
||||||
if (releaseAmount > 0)
|
if (releaseAmount > 0)
|
||||||
{
|
{
|
||||||
TryReleaseSemaphore(releaseAmount);
|
try
|
||||||
|
{
|
||||||
|
_semaphore.Release(releaseAmount);
|
||||||
|
}
|
||||||
|
catch (SemaphoreFullException)
|
||||||
|
{
|
||||||
|
// ignore, already at max
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_currentLimit = desiredLimit;
|
_currentLimit = desiredLimit;
|
||||||
_pendingReductions = 0;
|
_pendingReductions = 0;
|
||||||
_pendingIncrements = 0;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,13 +113,10 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
|||||||
if (desiredLimit > _currentLimit)
|
if (desiredLimit > _currentLimit)
|
||||||
{
|
{
|
||||||
var increment = desiredLimit - _currentLimit;
|
var increment = desiredLimit - _currentLimit;
|
||||||
_pendingIncrements += increment;
|
var allowed = Math.Min(increment, HardLimit - _semaphore.CurrentCount);
|
||||||
|
if (allowed > 0)
|
||||||
var available = HardLimit - _semaphore.CurrentCount;
|
|
||||||
var toRelease = Math.Min(_pendingIncrements, available);
|
|
||||||
if (toRelease > 0 && TryReleaseSemaphore(toRelease))
|
|
||||||
{
|
{
|
||||||
_pendingIncrements -= toRelease;
|
_semaphore.Release(allowed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -131,13 +133,6 @@ 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;
|
||||||
@@ -151,25 +146,6 @@ 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);
|
||||||
@@ -190,20 +166,9 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase
|
|||||||
_pendingReductions--;
|
_pendingReductions--;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_pendingIncrements > 0)
|
|
||||||
{
|
|
||||||
if (!TryReleaseSemaphore())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_pendingIncrements--;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TryReleaseSemaphore();
|
_semaphore.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
|
|||||||
@@ -19,12 +19,7 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
private static readonly TimeSpan Expiration = TimeSpan.FromMinutes(5);
|
private static readonly TimeSpan Expiration = TimeSpan.FromMinutes(5);
|
||||||
|
|
||||||
public PairRequestService(
|
public PairRequestService(ILogger<PairRequestService> logger, LightlessMediator mediator, DalamudUtilService dalamudUtil, PairManager pairManager, Lazy<WebAPI.ApiController> apiController)
|
||||||
ILogger<PairRequestService> logger,
|
|
||||||
LightlessMediator mediator,
|
|
||||||
DalamudUtilService dalamudUtil,
|
|
||||||
PairManager pairManager,
|
|
||||||
Lazy<WebAPI.ApiController> apiController)
|
|
||||||
: base(logger, mediator)
|
: base(logger, mediator)
|
||||||
{
|
{
|
||||||
_dalamudUtil = dalamudUtil;
|
_dalamudUtil = dalamudUtil;
|
||||||
@@ -75,6 +70,10 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase
|
|||||||
: _dalamudUtil.RunOnFrameworkThread(() => ToDisplay(entry)).GetAwaiter().GetResult();
|
: _dalamudUtil.RunOnFrameworkThread(() => ToDisplay(entry)).GetAwaiter().GetResult();
|
||||||
|
|
||||||
Mediator.Publish(new PairRequestsUpdatedMessage());
|
Mediator.Publish(new PairRequestsUpdatedMessage());
|
||||||
|
|
||||||
|
var senderName = string.IsNullOrEmpty(display.DisplayName) ? "Unknown User" : display.DisplayName;
|
||||||
|
Mediator.Publish(new PairRequestReceivedMessage(senderName, display.HashedCid));
|
||||||
|
|
||||||
return display;
|
return display;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,13 +219,9 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DeclinePairRequest(string hashedCid, string displayName)
|
public void DeclinePairRequest(string hashedCid)
|
||||||
{
|
{
|
||||||
RemoveRequest(hashedCid);
|
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);
|
Logger.LogDebug("Declined pair request from {HashedCid}", hashedCid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Dalamud.Interface.ImGuiFileDialog;
|
using LightlessSync.API.Dto.Group;
|
||||||
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;
|
||||||
@@ -19,11 +18,10 @@ 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, FileDialogManager fileDialogManager)
|
LightlessProfileManager lightlessProfileManager, PerformanceCollectorService performanceCollectorService)
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
_lightlessMediator = lightlessMediator;
|
_lightlessMediator = lightlessMediator;
|
||||||
@@ -33,13 +31,12 @@ 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, _lightlessProfileManager, _fileDialogManager);
|
_apiController, _uiSharedService, _pairManager, dto, _performanceCollectorService);
|
||||||
}
|
}
|
||||||
|
|
||||||
public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair)
|
public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair)
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ public sealed class UiService : DisposableMediatorSubscriberBase
|
|||||||
LightlessConfigService lightlessConfigService, WindowSystem windowSystem,
|
LightlessConfigService lightlessConfigService, WindowSystem windowSystem,
|
||||||
IEnumerable<WindowMediatorSubscriberBase> windows,
|
IEnumerable<WindowMediatorSubscriberBase> windows,
|
||||||
UiFactory uiFactory, FileDialogManager fileDialogManager,
|
UiFactory uiFactory, FileDialogManager fileDialogManager,
|
||||||
LightlessMediator lightlessMediator) : base(logger, lightlessMediator)
|
LightlessMediator lightlessMediator,
|
||||||
|
NotificationService notificationService) : base(logger, lightlessMediator)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_logger.LogTrace("Creating {type}", GetType().Name);
|
_logger.LogTrace("Creating {type}", GetType().Name);
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
private readonly BroadcastService _broadcastService;
|
private readonly BroadcastService _broadcastService;
|
||||||
|
|
||||||
private List<IDrawFolder> _drawFolders;
|
private List<IDrawFolder> _drawFolders;
|
||||||
|
private Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>>? _cachedAnalysis;
|
||||||
private Pair? _lastAddedUser;
|
private Pair? _lastAddedUser;
|
||||||
private string _lastAddedUserComment = string.Empty;
|
private string _lastAddedUserComment = string.Empty;
|
||||||
private Vector2 _lastPosition = Vector2.One;
|
private Vector2 _lastPosition = Vector2.One;
|
||||||
@@ -381,26 +382,15 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
_uiSharedService.IconText(FontAwesomeIcon.Upload);
|
_uiSharedService.IconText(FontAwesomeIcon.Upload);
|
||||||
ImGui.SameLine(35 * ImGuiHelpers.GlobalScale);
|
ImGui.SameLine(35 * ImGuiHelpers.GlobalScale);
|
||||||
|
|
||||||
if (currentUploads.Count > 0)
|
if (currentUploads.Any())
|
||||||
{
|
{
|
||||||
int totalUploads = currentUploads.Count;
|
var totalUploads = currentUploads.Count;
|
||||||
int doneUploads = 0;
|
|
||||||
long totalUploaded = 0;
|
|
||||||
long totalToUpload = 0;
|
|
||||||
|
|
||||||
foreach (var upload in currentUploads)
|
var doneUploads = currentUploads.Count(c => c.IsTransferred);
|
||||||
{
|
var activeUploads = currentUploads.Count(c => !c.IsTransferred);
|
||||||
if (upload.IsTransferred)
|
|
||||||
{
|
|
||||||
doneUploads++;
|
|
||||||
}
|
|
||||||
|
|
||||||
totalUploaded += upload.Transferred;
|
|
||||||
totalToUpload += upload.Total;
|
|
||||||
}
|
|
||||||
|
|
||||||
int activeUploads = totalUploads - doneUploads;
|
|
||||||
var uploadSlotLimit = Math.Clamp(_configService.Current.ParallelUploads, 1, 8);
|
var uploadSlotLimit = Math.Clamp(_configService.Current.ParallelUploads, 1, 8);
|
||||||
|
var totalUploaded = currentUploads.Sum(c => c.Transferred);
|
||||||
|
var totalToUpload = currentUploads.Sum(c => c.Total);
|
||||||
|
|
||||||
ImGui.TextUnformatted($"{doneUploads}/{totalUploads} (slots {activeUploads}/{uploadSlotLimit})");
|
ImGui.TextUnformatted($"{doneUploads}/{totalUploads} (slots {activeUploads}/{uploadSlotLimit})");
|
||||||
var uploadText = $"({UiSharedService.ByteToString(totalUploaded)}/{UiSharedService.ByteToString(totalToUpload)})";
|
var uploadText = $"({UiSharedService.ByteToString(totalUploaded)}/{UiSharedService.ByteToString(totalToUpload)})";
|
||||||
@@ -415,17 +405,17 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.TextUnformatted("No uploads in progress");
|
ImGui.TextUnformatted("No uploads in progress");
|
||||||
}
|
}
|
||||||
|
|
||||||
var downloadSummary = GetDownloadSummary();
|
var currentDownloads = BuildCurrentDownloadSnapshot();
|
||||||
ImGui.AlignTextToFramePadding();
|
ImGui.AlignTextToFramePadding();
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.Download);
|
_uiSharedService.IconText(FontAwesomeIcon.Download);
|
||||||
ImGui.SameLine(35 * ImGuiHelpers.GlobalScale);
|
ImGui.SameLine(35 * ImGuiHelpers.GlobalScale);
|
||||||
|
|
||||||
if (downloadSummary.HasDownloads)
|
if (currentDownloads.Any())
|
||||||
{
|
{
|
||||||
var totalDownloads = downloadSummary.TotalFiles;
|
var totalDownloads = currentDownloads.Sum(c => c.TotalFiles);
|
||||||
var doneDownloads = downloadSummary.TransferredFiles;
|
var doneDownloads = currentDownloads.Sum(c => c.TransferredFiles);
|
||||||
var totalDownloaded = downloadSummary.TransferredBytes;
|
var totalDownloaded = currentDownloads.Sum(c => c.TransferredBytes);
|
||||||
var totalToDownload = downloadSummary.TotalBytes;
|
var totalToDownload = currentDownloads.Sum(c => c.TotalBytes);
|
||||||
|
|
||||||
ImGui.TextUnformatted($"{doneDownloads}/{totalDownloads}");
|
ImGui.TextUnformatted($"{doneDownloads}/{totalDownloads}");
|
||||||
var downloadText =
|
var downloadText =
|
||||||
@@ -443,35 +433,27 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private DownloadSummary GetDownloadSummary()
|
private List<FileDownloadStatus> BuildCurrentDownloadSnapshot()
|
||||||
{
|
{
|
||||||
long totalBytes = 0;
|
List<FileDownloadStatus> snapshot = new();
|
||||||
long transferredBytes = 0;
|
|
||||||
int totalFiles = 0;
|
|
||||||
int transferredFiles = 0;
|
|
||||||
|
|
||||||
foreach (var kvp in _currentDownloads.ToArray())
|
foreach (var kvp in _currentDownloads.ToArray())
|
||||||
{
|
{
|
||||||
if (kvp.Value is not { Count: > 0 } statuses)
|
var value = kvp.Value;
|
||||||
{
|
if (value == null || value.Count == 0)
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var status in statuses.Values)
|
try
|
||||||
{
|
{
|
||||||
totalBytes += status.TotalBytes;
|
snapshot.AddRange(value.Values.ToArray());
|
||||||
transferredBytes += status.TransferredBytes;
|
}
|
||||||
totalFiles += status.TotalFiles;
|
catch (System.ArgumentException)
|
||||||
transferredFiles += status.TransferredFiles;
|
{
|
||||||
|
// skibidi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new DownloadSummary(totalFiles, transferredFiles, transferredBytes, totalBytes);
|
return snapshot;
|
||||||
}
|
|
||||||
|
|
||||||
private readonly record struct DownloadSummary(int TotalFiles, int TransferredFiles, long TransferredBytes, long TotalBytes)
|
|
||||||
{
|
|
||||||
public bool HasDownloads => TotalFiles > 0 || TotalBytes > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawUIDHeader()
|
private void DrawUIDHeader()
|
||||||
@@ -498,7 +480,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Getting information of character and triangles threshold to show overlimit status in UID bar.
|
//Getting information of character and triangles threshold to show overlimit status in UID bar.
|
||||||
var analysisSummary = _characterAnalyzer.LatestSummary;
|
_cachedAnalysis = _characterAnalyzer.LastAnalysis.DeepClone();
|
||||||
|
|
||||||
Vector2 uidTextSize, iconSize;
|
Vector2 uidTextSize, iconSize;
|
||||||
using (_uiSharedService.UidFont.Push())
|
using (_uiSharedService.UidFont.Push())
|
||||||
@@ -527,7 +509,6 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
{
|
{
|
||||||
ImGui.BeginTooltip();
|
ImGui.BeginTooltip();
|
||||||
ImGui.PushTextWrapPos(ImGui.GetFontSize() * 32f);
|
|
||||||
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("PairBlue"));
|
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("PairBlue"));
|
||||||
ImGui.Text("Lightfinder");
|
ImGui.Text("Lightfinder");
|
||||||
@@ -575,7 +556,6 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.PopStyleColor();
|
ImGui.PopStyleColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.PopTextWrapPos();
|
|
||||||
ImGui.EndTooltip();
|
ImGui.EndTooltip();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,7 +574,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
var seString = SeStringUtils.BuildFormattedPlayerName(uidText, vanityTextColor, vanityGlowColor);
|
var seString = SeStringUtils.BuildFormattedPlayerName(uidText, vanityTextColor, vanityGlowColor);
|
||||||
var cursorPos = ImGui.GetCursorScreenPos();
|
var cursorPos = ImGui.GetCursorScreenPos();
|
||||||
var fontPtr = ImGui.GetFont();
|
var fontPtr = ImGui.GetFont();
|
||||||
SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, fontPtr, "uid-header");
|
SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, fontPtr);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -611,40 +591,56 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
UiSharedService.AttachToolTip("Click to copy");
|
UiSharedService.AttachToolTip("Click to copy");
|
||||||
|
|
||||||
if (_apiController.ServerState is ServerState.Connected && analysisSummary.HasData)
|
if (_cachedAnalysis != null && _apiController.ServerState is ServerState.Connected)
|
||||||
{
|
{
|
||||||
var objectSummary = analysisSummary.Objects.Values.FirstOrDefault(summary => summary.HasEntries);
|
var firstEntry = _cachedAnalysis.FirstOrDefault();
|
||||||
if (objectSummary.HasEntries)
|
var valueDict = firstEntry.Value;
|
||||||
|
if (valueDict != null && valueDict.Count > 0)
|
||||||
{
|
{
|
||||||
var actualVramUsage = objectSummary.TexOriginalBytes;
|
var groupedfiles = valueDict
|
||||||
var actualTriCount = objectSummary.TotalTriangles;
|
.Select(v => v.Value)
|
||||||
|
.Where(v => v != null)
|
||||||
|
.GroupBy(f => f.FileType, StringComparer.Ordinal)
|
||||||
|
.OrderBy(k => k.Key, StringComparer.Ordinal)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
var isOverVRAMUsage = _playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024 < actualVramUsage;
|
var actualTriCount = valueDict
|
||||||
var isOverTriHold = actualTriCount > (_playerPerformanceConfig.Current.TrisWarningThresholdThousands * 1000);
|
.Select(v => v.Value)
|
||||||
|
.Where(v => v != null)
|
||||||
|
.Sum(f => f.Triangles);
|
||||||
|
|
||||||
if ((isOverTriHold || isOverVRAMUsage) && _playerPerformanceConfig.Current.WarnOnExceedingThresholds)
|
if (groupedfiles != null)
|
||||||
{
|
{
|
||||||
ImGui.SameLine();
|
//Checking of VRAM threshhold
|
||||||
ImGui.SetCursorPosY(cursorY + 15f);
|
var texGroup = groupedfiles.SingleOrDefault(v => string.Equals(v.Key, "tex", StringComparison.Ordinal));
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow"));
|
var actualVramUsage = texGroup != null ? texGroup.Sum(f => f.OriginalSize) : 0L;
|
||||||
|
var isOverVRAMUsage = _playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024 < actualVramUsage;
|
||||||
|
var isOverTriHold = actualTriCount > (_playerPerformanceConfig.Current.TrisWarningThresholdThousands * 1000);
|
||||||
|
|
||||||
string warningMessage = "";
|
if ((isOverTriHold || isOverVRAMUsage) && _playerPerformanceConfig.Current.WarnOnExceedingThresholds)
|
||||||
if (isOverTriHold)
|
|
||||||
{
|
{
|
||||||
warningMessage += $"You exceed your own triangles threshold by " +
|
ImGui.SameLine();
|
||||||
$"{actualTriCount - _playerPerformanceConfig.Current.TrisWarningThresholdThousands * 1000} triangles.";
|
ImGui.SetCursorPosY(cursorY + 15f);
|
||||||
warningMessage += Environment.NewLine;
|
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow"));
|
||||||
|
|
||||||
}
|
string warningMessage = "";
|
||||||
if (isOverVRAMUsage)
|
if (isOverTriHold)
|
||||||
{
|
{
|
||||||
warningMessage += $"You exceed your own VRAM threshold by " +
|
warningMessage += $"You exceed your own triangles threshold by " +
|
||||||
$"{UiSharedService.ByteToString(actualVramUsage - (_playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024))}.";
|
$"{actualTriCount - _playerPerformanceConfig.Current.TrisWarningThresholdThousands * 1000} triangles.";
|
||||||
}
|
warningMessage += Environment.NewLine;
|
||||||
UiSharedService.AttachToolTip(warningMessage);
|
|
||||||
if (ImGui.IsItemClicked())
|
}
|
||||||
{
|
if (isOverVRAMUsage)
|
||||||
_lightlessMediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi)));
|
{
|
||||||
|
warningMessage += $"You exceed your own VRAM threshold by " +
|
||||||
|
$"{UiSharedService.ByteToString(actualVramUsage - (_playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024))}.";
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip(warningMessage);
|
||||||
|
if (ImGui.IsItemClicked())
|
||||||
|
{
|
||||||
|
_lightlessMediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -667,7 +663,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
var seString = SeStringUtils.BuildFormattedPlayerName(_apiController.UID, vanityTextColor, vanityGlowColor);
|
var seString = SeStringUtils.BuildFormattedPlayerName(_apiController.UID, vanityTextColor, vanityGlowColor);
|
||||||
var cursorPos = ImGui.GetCursorScreenPos();
|
var cursorPos = ImGui.GetCursorScreenPos();
|
||||||
var fontPtr = ImGui.GetFont();
|
var fontPtr = ImGui.GetFont();
|
||||||
SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, fontPtr, "uid-footer");
|
SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, fontPtr);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -925,4 +921,4 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
_wasOpen = IsOpen;
|
_wasOpen = IsOpen;
|
||||||
IsOpen = false;
|
IsOpen = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
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.API.Dto.User;
|
||||||
@@ -14,9 +13,6 @@ using LightlessSync.Services.ServerConfiguration;
|
|||||||
using LightlessSync.UI.Handlers;
|
using LightlessSync.UI.Handlers;
|
||||||
using LightlessSync.Utils;
|
using LightlessSync.Utils;
|
||||||
using LightlessSync.WebAPI;
|
using LightlessSync.WebAPI;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.Immutable;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace LightlessSync.UI.Components;
|
namespace LightlessSync.UI.Components;
|
||||||
|
|
||||||
@@ -36,8 +32,6 @@ public class DrawUserPair
|
|||||||
private readonly CharaDataManager _charaDataManager;
|
private readonly CharaDataManager _charaDataManager;
|
||||||
private float _menuWidth = -1;
|
private float _menuWidth = -1;
|
||||||
private bool _wasHovered = false;
|
private bool _wasHovered = false;
|
||||||
private TooltipSnapshot _tooltipSnapshot = TooltipSnapshot.Empty;
|
|
||||||
private string _cachedTooltip = string.Empty;
|
|
||||||
|
|
||||||
public DrawUserPair(string id, Pair entry, List<GroupFullInfoDto> syncedGroups,
|
public DrawUserPair(string id, Pair entry, List<GroupFullInfoDto> syncedGroups,
|
||||||
GroupFullInfoDto? currentGroup,
|
GroupFullInfoDto? currentGroup,
|
||||||
@@ -196,12 +190,15 @@ public class DrawUserPair
|
|||||||
|
|
||||||
private void DrawLeftSide()
|
private void DrawLeftSide()
|
||||||
{
|
{
|
||||||
|
string userPairText = string.Empty;
|
||||||
|
|
||||||
ImGui.AlignTextToFramePadding();
|
ImGui.AlignTextToFramePadding();
|
||||||
|
|
||||||
if (_pair.IsPaused)
|
if (_pair.IsPaused)
|
||||||
{
|
{
|
||||||
using var _ = ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"));
|
using var _ = ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"));
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.PauseCircle);
|
_uiSharedService.IconText(FontAwesomeIcon.PauseCircle);
|
||||||
|
userPairText = _pair.UserData.AliasOrUID + " is paused";
|
||||||
}
|
}
|
||||||
else if (!_pair.IsOnline)
|
else if (!_pair.IsOnline)
|
||||||
{
|
{
|
||||||
@@ -210,10 +207,12 @@ public class DrawUserPair
|
|||||||
? FontAwesomeIcon.ArrowsLeftRight
|
? FontAwesomeIcon.ArrowsLeftRight
|
||||||
: (_pair.IndividualPairStatus == API.Data.Enum.IndividualPairStatus.Bidirectional
|
: (_pair.IndividualPairStatus == API.Data.Enum.IndividualPairStatus.Bidirectional
|
||||||
? FontAwesomeIcon.User : FontAwesomeIcon.Users));
|
? FontAwesomeIcon.User : FontAwesomeIcon.Users));
|
||||||
|
userPairText = _pair.UserData.AliasOrUID + " is offline";
|
||||||
}
|
}
|
||||||
else if (_pair.IsVisible)
|
else if (_pair.IsVisible)
|
||||||
{
|
{
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.Eye, UIColors.Get("LightlessBlue"));
|
_uiSharedService.IconText(FontAwesomeIcon.Eye, UIColors.Get("LightlessBlue"));
|
||||||
|
userPairText = _pair.UserData.AliasOrUID + " is visible: " + _pair.PlayerName + Environment.NewLine + "Click to target this player";
|
||||||
if (ImGui.IsItemClicked())
|
if (ImGui.IsItemClicked())
|
||||||
{
|
{
|
||||||
_mediator.Publish(new TargetPairMessage(_pair));
|
_mediator.Publish(new TargetPairMessage(_pair));
|
||||||
@@ -224,9 +223,46 @@ public class DrawUserPair
|
|||||||
using var _ = ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("PairBlue"));
|
using var _ = ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("PairBlue"));
|
||||||
_uiSharedService.IconText(_pair.IndividualPairStatus == API.Data.Enum.IndividualPairStatus.Bidirectional
|
_uiSharedService.IconText(_pair.IndividualPairStatus == API.Data.Enum.IndividualPairStatus.Bidirectional
|
||||||
? FontAwesomeIcon.User : FontAwesomeIcon.Users);
|
? FontAwesomeIcon.User : FontAwesomeIcon.Users);
|
||||||
|
userPairText = _pair.UserData.AliasOrUID + " is online";
|
||||||
}
|
}
|
||||||
|
|
||||||
UiSharedService.AttachToolTip(GetUserTooltip());
|
if (_pair.IndividualPairStatus == API.Data.Enum.IndividualPairStatus.OneSided)
|
||||||
|
{
|
||||||
|
userPairText += UiSharedService.TooltipSeparator + "User has not added you back";
|
||||||
|
}
|
||||||
|
else if (_pair.IndividualPairStatus == API.Data.Enum.IndividualPairStatus.Bidirectional)
|
||||||
|
{
|
||||||
|
userPairText += UiSharedService.TooltipSeparator + "You are directly Paired";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_pair.LastAppliedDataBytes >= 0)
|
||||||
|
{
|
||||||
|
userPairText += UiSharedService.TooltipSeparator;
|
||||||
|
userPairText += ((!_pair.IsPaired) ? "(Last) " : string.Empty) + "Mods Info" + Environment.NewLine;
|
||||||
|
userPairText += "Files Size: " + UiSharedService.ByteToString(_pair.LastAppliedDataBytes, true);
|
||||||
|
if (_pair.LastAppliedApproximateVRAMBytes >= 0)
|
||||||
|
{
|
||||||
|
userPairText += Environment.NewLine + "Approx. VRAM Usage: " + UiSharedService.ByteToString(_pair.LastAppliedApproximateVRAMBytes, true);
|
||||||
|
}
|
||||||
|
if (_pair.LastAppliedDataTris >= 0)
|
||||||
|
{
|
||||||
|
userPairText += Environment.NewLine + "Approx. Triangle Count (excl. Vanilla): "
|
||||||
|
+ (_pair.LastAppliedDataTris > 1000 ? (_pair.LastAppliedDataTris / 1000d).ToString("0.0'k'") : _pair.LastAppliedDataTris);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_syncedGroups.Any())
|
||||||
|
{
|
||||||
|
userPairText += UiSharedService.TooltipSeparator + string.Join(Environment.NewLine,
|
||||||
|
_syncedGroups.Select(g =>
|
||||||
|
{
|
||||||
|
var groupNote = _serverConfigurationManager.GetNoteForGid(g.GID);
|
||||||
|
var groupString = string.IsNullOrEmpty(groupNote) ? g.GroupAliasOrGID : $"{groupNote} ({g.GroupAliasOrGID})";
|
||||||
|
return "Paired through " + groupString;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
UiSharedService.AttachToolTip(userPairText);
|
||||||
|
|
||||||
if (_performanceConfigService.Current.ShowPerformanceIndicator
|
if (_performanceConfigService.Current.ShowPerformanceIndicator
|
||||||
&& !_performanceConfigService.Current.UIDsToIgnore
|
&& !_performanceConfigService.Current.UIDsToIgnore
|
||||||
@@ -291,143 +327,6 @@ public class DrawUserPair
|
|||||||
_displayHandler.DrawPairText(_id, _pair, leftSide, () => rightSide - leftSide);
|
_displayHandler.DrawPairText(_id, _pair, leftSide, () => rightSide - leftSide);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetUserTooltip()
|
|
||||||
{
|
|
||||||
List<string>? groupDisplays = null;
|
|
||||||
if (_syncedGroups.Count > 0)
|
|
||||||
{
|
|
||||||
groupDisplays = new List<string>(_syncedGroups.Count);
|
|
||||||
foreach (var group in _syncedGroups)
|
|
||||||
{
|
|
||||||
var groupNote = _serverConfigurationManager.GetNoteForGid(group.GID);
|
|
||||||
groupDisplays.Add(string.IsNullOrEmpty(groupNote) ? group.GroupAliasOrGID : $"{groupNote} ({group.GroupAliasOrGID})");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var snapshot = new TooltipSnapshot(
|
|
||||||
_pair.IsPaused,
|
|
||||||
_pair.IsOnline,
|
|
||||||
_pair.IsVisible,
|
|
||||||
_pair.IndividualPairStatus,
|
|
||||||
_pair.UserData.AliasOrUID,
|
|
||||||
_pair.PlayerName ?? string.Empty,
|
|
||||||
_pair.LastAppliedDataBytes,
|
|
||||||
_pair.LastAppliedApproximateVRAMBytes,
|
|
||||||
_pair.LastAppliedDataTris,
|
|
||||||
_pair.IsPaired,
|
|
||||||
groupDisplays is null ? ImmutableArray<string>.Empty : ImmutableArray.CreateRange(groupDisplays));
|
|
||||||
|
|
||||||
if (!_tooltipSnapshot.Equals(snapshot))
|
|
||||||
{
|
|
||||||
_cachedTooltip = BuildTooltip(snapshot);
|
|
||||||
_tooltipSnapshot = snapshot;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _cachedTooltip;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BuildTooltip(in TooltipSnapshot snapshot)
|
|
||||||
{
|
|
||||||
var builder = new StringBuilder(256);
|
|
||||||
|
|
||||||
if (snapshot.IsPaused)
|
|
||||||
{
|
|
||||||
builder.Append(snapshot.AliasOrUid);
|
|
||||||
builder.Append(" is paused");
|
|
||||||
}
|
|
||||||
else if (!snapshot.IsOnline)
|
|
||||||
{
|
|
||||||
builder.Append(snapshot.AliasOrUid);
|
|
||||||
builder.Append(" is offline");
|
|
||||||
}
|
|
||||||
else if (snapshot.IsVisible)
|
|
||||||
{
|
|
||||||
builder.Append(snapshot.AliasOrUid);
|
|
||||||
builder.Append(" is visible: ");
|
|
||||||
builder.Append(snapshot.PlayerName);
|
|
||||||
builder.Append(Environment.NewLine);
|
|
||||||
builder.Append("Click to target this player");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
builder.Append(snapshot.AliasOrUid);
|
|
||||||
builder.Append(" is online");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.PairStatus == IndividualPairStatus.OneSided)
|
|
||||||
{
|
|
||||||
builder.Append(UiSharedService.TooltipSeparator);
|
|
||||||
builder.Append("User has not added you back");
|
|
||||||
}
|
|
||||||
else if (snapshot.PairStatus == IndividualPairStatus.Bidirectional)
|
|
||||||
{
|
|
||||||
builder.Append(UiSharedService.TooltipSeparator);
|
|
||||||
builder.Append("You are directly Paired");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.LastAppliedDataBytes >= 0)
|
|
||||||
{
|
|
||||||
builder.Append(UiSharedService.TooltipSeparator);
|
|
||||||
if (!snapshot.IsPaired)
|
|
||||||
{
|
|
||||||
builder.Append("(Last) ");
|
|
||||||
}
|
|
||||||
builder.Append("Mods Info");
|
|
||||||
builder.Append(Environment.NewLine);
|
|
||||||
builder.Append("Files Size: ");
|
|
||||||
builder.Append(UiSharedService.ByteToString(snapshot.LastAppliedDataBytes, true));
|
|
||||||
|
|
||||||
if (snapshot.LastAppliedApproximateVRAMBytes >= 0)
|
|
||||||
{
|
|
||||||
builder.Append(Environment.NewLine);
|
|
||||||
builder.Append("Approx. VRAM Usage: ");
|
|
||||||
builder.Append(UiSharedService.ByteToString(snapshot.LastAppliedApproximateVRAMBytes, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.LastAppliedDataTris >= 0)
|
|
||||||
{
|
|
||||||
builder.Append(Environment.NewLine);
|
|
||||||
builder.Append("Approx. Triangle Count (excl. Vanilla): ");
|
|
||||||
builder.Append(snapshot.LastAppliedDataTris > 1000
|
|
||||||
? (snapshot.LastAppliedDataTris / 1000d).ToString("0.0'k'")
|
|
||||||
: snapshot.LastAppliedDataTris);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!snapshot.GroupDisplays.IsEmpty)
|
|
||||||
{
|
|
||||||
builder.Append(UiSharedService.TooltipSeparator);
|
|
||||||
for (int i = 0; i < snapshot.GroupDisplays.Length; i++)
|
|
||||||
{
|
|
||||||
if (i > 0)
|
|
||||||
{
|
|
||||||
builder.Append(Environment.NewLine);
|
|
||||||
}
|
|
||||||
builder.Append("Paired through ");
|
|
||||||
builder.Append(snapshot.GroupDisplays[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly record struct TooltipSnapshot(
|
|
||||||
bool IsPaused,
|
|
||||||
bool IsOnline,
|
|
||||||
bool IsVisible,
|
|
||||||
IndividualPairStatus PairStatus,
|
|
||||||
string AliasOrUid,
|
|
||||||
string PlayerName,
|
|
||||||
long LastAppliedDataBytes,
|
|
||||||
long LastAppliedApproximateVRAMBytes,
|
|
||||||
long LastAppliedDataTris,
|
|
||||||
bool IsPaired,
|
|
||||||
ImmutableArray<string> GroupDisplays)
|
|
||||||
{
|
|
||||||
public static TooltipSnapshot Empty { get; } =
|
|
||||||
new(false, false, false, IndividualPairStatus.None, string.Empty, string.Empty, -1, -1, -1, false, ImmutableArray<string>.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawPairedClientMenu()
|
private void DrawPairedClientMenu()
|
||||||
{
|
{
|
||||||
DrawIndividualMenu();
|
DrawIndividualMenu();
|
||||||
|
|||||||
@@ -22,12 +22,13 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
private readonly UiSharedService _uiShared;
|
private readonly UiSharedService _uiShared;
|
||||||
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||||
private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new();
|
private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new();
|
||||||
|
private readonly NotificationService _notificationService;
|
||||||
private bool _notificationDismissed = true;
|
private bool _notificationDismissed = true;
|
||||||
private int _lastDownloadStateHash = 0;
|
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,
|
PairProcessingLimiter pairProcessingLimiter, FileUploadManager fileTransferManager, LightlessMediator mediator, UiSharedService uiShared,
|
||||||
PerformanceCollectorService performanceCollectorService)
|
PerformanceCollectorService performanceCollectorService, NotificationService notificationService)
|
||||||
: base(logger, mediator, "Lightless Sync Downloads", performanceCollectorService)
|
: base(logger, mediator, "Lightless Sync Downloads", performanceCollectorService)
|
||||||
{
|
{
|
||||||
_dalamudUtilService = dalamudUtilService;
|
_dalamudUtilService = dalamudUtilService;
|
||||||
@@ -35,6 +36,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
_pairProcessingLimiter = pairProcessingLimiter;
|
_pairProcessingLimiter = pairProcessingLimiter;
|
||||||
_fileTransferManager = fileTransferManager;
|
_fileTransferManager = fileTransferManager;
|
||||||
_uiShared = uiShared;
|
_uiShared = uiShared;
|
||||||
|
_notificationService = notificationService;
|
||||||
|
|
||||||
SizeConstraints = new WindowSizeConstraints()
|
SizeConstraints = new WindowSizeConstraints()
|
||||||
{
|
{
|
||||||
@@ -357,7 +359,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
_lastDownloadStateHash = currentHash;
|
_lastDownloadStateHash = currentHash;
|
||||||
if (downloadStatus.Count > 0 || queueWaiting > 0)
|
if (downloadStatus.Count > 0 || queueWaiting > 0)
|
||||||
{
|
{
|
||||||
Mediator.Publish(new PairDownloadStatusMessage(downloadStatus, queueWaiting));
|
_notificationService.ShowPairDownloadNotification(downloadStatus, queueWaiting);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<ClearProfileUserDataMessage>(this, (msg) =>
|
Mediator.Subscribe<ClearProfileDataMessage>(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,7 +91,6 @@ 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));
|
||||||
|
|
||||||
@@ -109,8 +108,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
ImGui.Dummy(new Vector2(3));
|
ImGui.Dummy(new Vector2(3));
|
||||||
|
|
||||||
var profile = _lightlessProfileManager.GetLightlessUserProfile(new UserData(_apiController.UID));
|
var profile = _lightlessProfileManager.GetLightlessProfile(new UserData(_apiController.UID));
|
||||||
_logger.LogInformation("Profile fetched for drawing: {profile}", profile);
|
|
||||||
|
|
||||||
if (ImGui.BeginTabBar("##EditProfileTabs"))
|
if (ImGui.BeginTabBar("##EditProfileTabs"))
|
||||||
{
|
{
|
||||||
@@ -206,7 +204,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), BannerPictureBase64: null, Description: null, Tags: null))
|
await _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, Convert.ToBase64String(fileContent), Description: null))
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -215,7 +213,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, BannerPictureBase64: null, Tags: null));
|
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, "", Description: null));
|
||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("Clear your currently uploaded profile picture");
|
UiSharedService.AttachToolTip("Clear your currently uploaded profile picture");
|
||||||
if (_showFileDialogError)
|
if (_showFileDialogError)
|
||||||
@@ -225,7 +223,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, BannerPictureBase64: null, Tags: null));
|
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, isNsfw, ProfilePictureBase64: null, Description: null));
|
||||||
}
|
}
|
||||||
_uiSharedService.DrawHelpText("If your profile description or image can be considered NSFW, toggle this to ON");
|
_uiSharedService.DrawHelpText("If your profile description or image can be considered NSFW, toggle this to ON");
|
||||||
var widthTextBox = 400;
|
var widthTextBox = 400;
|
||||||
@@ -264,13 +262,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, BannerPictureBase64: null, _descriptionText, Tags: null));
|
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, _descriptionText));
|
||||||
}
|
}
|
||||||
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, BannerPictureBase64: null, "", Tags: null));
|
_ = _apiController.UserSetProfile(new UserProfileDto(new UserData(_apiController.UID), Disabled: false, IsNSFW: null, ProfilePictureBase64: null, ""));
|
||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("Clears your profile description text");
|
UiSharedService.AttachToolTip("Clears your profile description text");
|
||||||
|
|
||||||
@@ -281,7 +279,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. If you have the vanity role, you must interact with the Discord bot first.");
|
_uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessPurple"), "Must be a supporter through Patreon/Ko-fi to access these settings.");
|
||||||
|
|
||||||
var hasVanity = _apiController.HasVanity;
|
var hasVanity = _apiController.HasVanity;
|
||||||
|
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ public class IdDisplayHandler
|
|||||||
Vector2 textSize;
|
Vector2 textSize;
|
||||||
using (ImRaii.PushFont(font, textIsUid))
|
using (ImRaii.PushFont(font, textIsUid))
|
||||||
{
|
{
|
||||||
SeStringUtils.RenderSeStringWithHitbox(seString, rowStart, font, pair.UserData.UID);
|
SeStringUtils.RenderSeStringWithHitbox(seString, rowStart, font);
|
||||||
itemMin = ImGui.GetItemRectMin();
|
itemMin = ImGui.GetItemRectMin();
|
||||||
itemMax = ImGui.GetItemRectMax();
|
itemMax = ImGui.GetItemRectMax();
|
||||||
//textSize = itemMax - itemMin;
|
//textSize = itemMax - itemMin;
|
||||||
|
|||||||
@@ -15,17 +15,17 @@ using Dalamud.Bindings.ImGui;
|
|||||||
|
|
||||||
namespace LightlessSync.UI;
|
namespace LightlessSync.UI;
|
||||||
|
|
||||||
public class LightlessNotificationUi : WindowMediatorSubscriberBase
|
public class LightlessNotificationUI : WindowMediatorSubscriberBase
|
||||||
{
|
{
|
||||||
private const float _notificationMinHeight = 60f;
|
private const float NotificationMinHeight = 60f;
|
||||||
private const float _notificationMaxHeight = 250f;
|
private const float NotificationMaxHeight = 250f;
|
||||||
private const float _windowPaddingOffset = 6f;
|
private const float WindowPaddingOffset = 6f;
|
||||||
private const float _slideAnimationDistance = 100f;
|
private const float SlideAnimationDistance = 100f;
|
||||||
private const float _outAnimationSpeedMultiplier = 0.7f;
|
private const float OutAnimationSpeedMultiplier = 0.7f;
|
||||||
private const float _contentPaddingX = 10f;
|
private const float ContentPaddingX = 10f;
|
||||||
private const float _contentPaddingY = 6f;
|
private const float ContentPaddingY = 6f;
|
||||||
private const float _titleMessageSpacing = 4f;
|
private const float TitleMessageSpacing = 4f;
|
||||||
private const float _actionButtonSpacing = 8f;
|
private const float ActionButtonSpacing = 8f;
|
||||||
|
|
||||||
private readonly List<LightlessNotification> _notifications = new();
|
private readonly List<LightlessNotification> _notifications = new();
|
||||||
private readonly object _notificationLock = new();
|
private readonly object _notificationLock = new();
|
||||||
@@ -33,7 +33,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
|
|||||||
private readonly Dictionary<string, float> _notificationYOffsets = new();
|
private readonly Dictionary<string, float> _notificationYOffsets = new();
|
||||||
private readonly Dictionary<string, float> _notificationTargetYOffsets = new();
|
private readonly Dictionary<string, float> _notificationTargetYOffsets = new();
|
||||||
|
|
||||||
public LightlessNotificationUi(ILogger<LightlessNotificationUi> logger, LightlessMediator mediator, PerformanceCollectorService performanceCollector, LightlessConfigService configService)
|
public LightlessNotificationUI(ILogger<LightlessNotificationUI> logger, LightlessMediator mediator, PerformanceCollectorService performanceCollector, LightlessConfigService configService)
|
||||||
: base(logger, mediator, "Lightless Notifications##LightlessNotifications", performanceCollector)
|
: base(logger, mediator, "Lightless Notifications##LightlessNotifications", performanceCollector)
|
||||||
{
|
{
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
@@ -155,8 +155,8 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
|
|||||||
var width = _configService.Current.NotificationWidth;
|
var width = _configService.Current.NotificationWidth;
|
||||||
|
|
||||||
float posX = corner == NotificationCorner.Left
|
float posX = corner == NotificationCorner.Left
|
||||||
? viewport.WorkPos.X + offsetX - _windowPaddingOffset
|
? viewport.WorkPos.X + offsetX - WindowPaddingOffset
|
||||||
: viewport.WorkPos.X + viewport.WorkSize.X - width - offsetX - _windowPaddingOffset;
|
: viewport.WorkPos.X + viewport.WorkSize.X - width - offsetX - WindowPaddingOffset;
|
||||||
|
|
||||||
return new Vector2(posX, viewport.WorkPos.Y);
|
return new Vector2(posX, viewport.WorkPos.Y);
|
||||||
}
|
}
|
||||||
@@ -274,7 +274,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
|
|||||||
else if (notification.IsAnimatingOut && notification.AnimationProgress > 0f)
|
else if (notification.IsAnimatingOut && notification.AnimationProgress > 0f)
|
||||||
{
|
{
|
||||||
notification.AnimationProgress = Math.Max(0f,
|
notification.AnimationProgress = Math.Max(0f,
|
||||||
notification.AnimationProgress - deltaTime * _configService.Current.NotificationAnimationSpeed * _outAnimationSpeedMultiplier);
|
notification.AnimationProgress - deltaTime * _configService.Current.NotificationAnimationSpeed * OutAnimationSpeedMultiplier);
|
||||||
}
|
}
|
||||||
else if (!notification.IsAnimatingOut && !notification.IsDismissed)
|
else if (!notification.IsAnimatingOut && !notification.IsDismissed)
|
||||||
{
|
{
|
||||||
@@ -289,7 +289,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
private Vector2 CalculateSlideOffset(float alpha)
|
private Vector2 CalculateSlideOffset(float alpha)
|
||||||
{
|
{
|
||||||
var distance = (1f - alpha) * _slideAnimationDistance;
|
var distance = (1f - alpha) * SlideAnimationDistance;
|
||||||
var corner = _configService.Current.NotificationCorner;
|
var corner = _configService.Current.NotificationCorner;
|
||||||
return corner == NotificationCorner.Left ? new Vector2(-distance, 0) : new Vector2(distance, 0);
|
return corner == NotificationCorner.Left ? new Vector2(-distance, 0) : new Vector2(distance, 0);
|
||||||
}
|
}
|
||||||
@@ -466,7 +466,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
private void DrawNotificationText(LightlessNotification notification, float alpha)
|
private void DrawNotificationText(LightlessNotification notification, float alpha)
|
||||||
{
|
{
|
||||||
var contentPos = new Vector2(_contentPaddingX, _contentPaddingY);
|
var contentPos = new Vector2(ContentPaddingX, ContentPaddingY);
|
||||||
var windowSize = ImGui.GetWindowSize();
|
var windowSize = ImGui.GetWindowSize();
|
||||||
var contentWidth = CalculateContentWidth(windowSize.X);
|
var contentWidth = CalculateContentWidth(windowSize.X);
|
||||||
|
|
||||||
@@ -483,7 +483,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
private float CalculateContentWidth(float windowWidth) =>
|
private float CalculateContentWidth(float windowWidth) =>
|
||||||
windowWidth - (_contentPaddingX * 2);
|
windowWidth - (ContentPaddingX * 2);
|
||||||
|
|
||||||
private bool HasActions(LightlessNotification notification) =>
|
private bool HasActions(LightlessNotification notification) =>
|
||||||
notification.Actions.Count > 0;
|
notification.Actions.Count > 0;
|
||||||
@@ -491,9 +491,9 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
|
|||||||
private void PositionActionsAtBottom(float windowHeight)
|
private void PositionActionsAtBottom(float windowHeight)
|
||||||
{
|
{
|
||||||
var actionHeight = ImGui.GetFrameHeight();
|
var actionHeight = ImGui.GetFrameHeight();
|
||||||
var bottomY = windowHeight - _contentPaddingY - actionHeight;
|
var bottomY = windowHeight - ContentPaddingY - actionHeight;
|
||||||
ImGui.SetCursorPosY(bottomY);
|
ImGui.SetCursorPosY(bottomY);
|
||||||
ImGui.SetCursorPosX(_contentPaddingX);
|
ImGui.SetCursorPosX(ContentPaddingX);
|
||||||
}
|
}
|
||||||
|
|
||||||
private float DrawTitle(LightlessNotification notification, float contentWidth, float alpha)
|
private float DrawTitle(LightlessNotification notification, float contentWidth, float alpha)
|
||||||
@@ -530,7 +530,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(notification.Message)) return;
|
if (string.IsNullOrEmpty(notification.Message)) return;
|
||||||
|
|
||||||
var messagePos = contentPos + new Vector2(0f, titleHeight + _titleMessageSpacing);
|
var messagePos = contentPos + new Vector2(0f, titleHeight + TitleMessageSpacing);
|
||||||
var messageColor = new Vector4(0.9f, 0.9f, 0.9f, alpha);
|
var messageColor = new Vector4(0.9f, 0.9f, 0.9f, alpha);
|
||||||
|
|
||||||
ImGui.SetCursorPos(messagePos);
|
ImGui.SetCursorPos(messagePos);
|
||||||
@@ -563,13 +563,13 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
private float CalculateActionButtonWidth(int actionCount, float availableWidth)
|
private float CalculateActionButtonWidth(int actionCount, float availableWidth)
|
||||||
{
|
{
|
||||||
var totalSpacing = (actionCount - 1) * _actionButtonSpacing;
|
var totalSpacing = (actionCount - 1) * ActionButtonSpacing;
|
||||||
return (availableWidth - totalSpacing) / actionCount;
|
return (availableWidth - totalSpacing) / actionCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PositionActionButton(int index, float startX, float buttonWidth)
|
private void PositionActionButton(int index, float startX, float buttonWidth)
|
||||||
{
|
{
|
||||||
var xPosition = startX + index * (buttonWidth + _actionButtonSpacing);
|
var xPosition = startX + index * (buttonWidth + ActionButtonSpacing);
|
||||||
ImGui.SetCursorPosX(xPosition);
|
ImGui.SetCursorPosX(xPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -687,7 +687,7 @@ public class LightlessNotificationUi : WindowMediatorSubscriberBase
|
|||||||
height += 12f;
|
height += 12f;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Math.Clamp(height, _notificationMinHeight, _notificationMaxHeight);
|
return Math.Clamp(height, NotificationMinHeight, NotificationMaxHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
private float CalculateTitleHeight(LightlessNotification notification, float contentWidth)
|
private float CalculateTitleHeight(LightlessNotification notification, float contentWidth)
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -85,7 +85,7 @@ public class PopoutProfileUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
var spacing = ImGui.GetStyle().ItemSpacing;
|
var spacing = ImGui.GetStyle().ItemSpacing;
|
||||||
|
|
||||||
var lightlessProfile = _lightlessProfileManager.GetLightlessUserProfile(_pair.UserData);
|
var lightlessProfile = _lightlessProfileManager.GetLightlessProfile(_pair.UserData);
|
||||||
|
|
||||||
if (_textureWrap == null || !lightlessProfile.ImageData.Value.SequenceEqual(_lastProfilePicture))
|
if (_textureWrap == null || !lightlessProfile.ImageData.Value.SequenceEqual(_lastProfilePicture))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
namespace LightlessSync.UI
|
|
||||||
{
|
|
||||||
public enum ProfileTags
|
|
||||||
{
|
|
||||||
SFW = 0,
|
|
||||||
NSFW = 1,
|
|
||||||
RP = 2,
|
|
||||||
ERP = 3,
|
|
||||||
Venues = 4,
|
|
||||||
Gpose = 5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -63,6 +63,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
private readonly IProgress<(int, int, FileCacheEntity)> _validationProgress;
|
private readonly IProgress<(int, int, FileCacheEntity)> _validationProgress;
|
||||||
private readonly NameplateService _nameplateService;
|
private readonly NameplateService _nameplateService;
|
||||||
private readonly NameplateHandler _nameplateHandler;
|
private readonly NameplateHandler _nameplateHandler;
|
||||||
|
private readonly NotificationService _lightlessNotificationService;
|
||||||
private (int, int, FileCacheEntity) _currentProgress;
|
private (int, int, FileCacheEntity) _currentProgress;
|
||||||
private bool _deleteAccountPopupModalShown = false;
|
private bool _deleteAccountPopupModalShown = false;
|
||||||
private bool _deleteFilesPopupModalShown = false;
|
private bool _deleteFilesPopupModalShown = false;
|
||||||
@@ -106,7 +107,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
IpcManager ipcManager, CacheMonitor cacheMonitor,
|
IpcManager ipcManager, CacheMonitor cacheMonitor,
|
||||||
DalamudUtilService dalamudUtilService, HttpClient httpClient,
|
DalamudUtilService dalamudUtilService, HttpClient httpClient,
|
||||||
NameplateService nameplateService,
|
NameplateService nameplateService,
|
||||||
NameplateHandler nameplateHandler) : base(logger, mediator, "Lightless Sync Settings",
|
NameplateHandler nameplateHandler,
|
||||||
|
NotificationService lightlessNotificationService) : base(logger, mediator, "Lightless Sync Settings",
|
||||||
performanceCollector)
|
performanceCollector)
|
||||||
{
|
{
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
@@ -128,6 +130,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
_uiShared = uiShared;
|
_uiShared = uiShared;
|
||||||
_nameplateService = nameplateService;
|
_nameplateService = nameplateService;
|
||||||
_nameplateHandler = nameplateHandler;
|
_nameplateHandler = nameplateHandler;
|
||||||
|
_lightlessNotificationService = lightlessNotificationService;
|
||||||
AllowClickthrough = false;
|
AllowClickthrough = false;
|
||||||
AllowPinning = true;
|
AllowPinning = true;
|
||||||
_validationProgress = new Progress<(int, int, FileCacheEntity)>(v => _currentProgress = v);
|
_validationProgress = new Progress<(int, int, FileCacheEntity)>(v => _currentProgress = v);
|
||||||
@@ -137,25 +140,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
MinimumSize = new Vector2(800, 400), MaximumSize = new Vector2(800, 2000),
|
MinimumSize = new Vector2(800, 400), MaximumSize = new Vector2(800, 2000),
|
||||||
};
|
};
|
||||||
|
|
||||||
TitleBarButtons = new()
|
|
||||||
{
|
|
||||||
new TitleBarButton()
|
|
||||||
{
|
|
||||||
Icon = FontAwesomeIcon.FileAlt,
|
|
||||||
Click = (msg) =>
|
|
||||||
{
|
|
||||||
Mediator.Publish(new UiToggleMessage(typeof(UpdateNotesUi)));
|
|
||||||
},
|
|
||||||
IconOffset = new(2, 1),
|
|
||||||
ShowTooltip = () =>
|
|
||||||
{
|
|
||||||
ImGui.BeginTooltip();
|
|
||||||
ImGui.Text("View Update Notes");
|
|
||||||
ImGui.EndTooltip();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Mediator.Subscribe<OpenSettingsUiMessage>(this, (_) => Toggle());
|
Mediator.Subscribe<OpenSettingsUiMessage>(this, (_) => Toggle());
|
||||||
Mediator.Subscribe<OpenLightfinderSettingsMessage>(this, (_) =>
|
Mediator.Subscribe<OpenLightfinderSettingsMessage>(this, (_) =>
|
||||||
{
|
{
|
||||||
@@ -607,7 +591,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
bool limitPairApplications = _configService.Current.EnablePairProcessingLimiter;
|
bool limitPairApplications = _configService.Current.EnablePairProcessingLimiter;
|
||||||
bool useAlternativeUpload = _configService.Current.UseAlternativeFileUpload;
|
bool useAlternativeUpload = _configService.Current.UseAlternativeFileUpload;
|
||||||
int downloadSpeedLimit = _configService.Current.DownloadSpeedLimitInBytes;
|
int downloadSpeedLimit = _configService.Current.DownloadSpeedLimitInBytes;
|
||||||
bool enableDirectDownloads = _configService.Current.EnableDirectDownloads;
|
|
||||||
|
|
||||||
ImGui.AlignTextToFramePadding();
|
ImGui.AlignTextToFramePadding();
|
||||||
ImGui.TextUnformatted("Global Download Speed Limit");
|
ImGui.TextUnformatted("Global Download Speed Limit");
|
||||||
@@ -639,13 +622,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.AlignTextToFramePadding();
|
ImGui.AlignTextToFramePadding();
|
||||||
ImGui.TextUnformatted("0 = No limit/infinite");
|
ImGui.TextUnformatted("0 = No limit/infinite");
|
||||||
|
|
||||||
if (ImGui.Checkbox("[BETA] Enable Lightspeed Downloads", ref enableDirectDownloads))
|
|
||||||
{
|
|
||||||
_configService.Current.EnableDirectDownloads = enableDirectDownloads;
|
|
||||||
_configService.Save();
|
|
||||||
}
|
|
||||||
_uiShared.DrawHelpText("Uses signed CDN links when available. Disable to force the legacy queued download flow.");
|
|
||||||
|
|
||||||
if (ImGui.SliderInt("Maximum Parallel Downloads", ref maxParallelDownloads, 1, 10))
|
if (ImGui.SliderInt("Maximum Parallel Downloads", ref maxParallelDownloads, 1, 10))
|
||||||
{
|
{
|
||||||
_configService.Current.ParallelDownloads = maxParallelDownloads;
|
_configService.Current.ParallelDownloads = maxParallelDownloads;
|
||||||
@@ -2318,7 +2294,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (ImGui.Checkbox("Show Lightless Profiles on Hover", ref showProfiles))
|
if (ImGui.Checkbox("Show Lightless Profiles on Hover", ref showProfiles))
|
||||||
{
|
{
|
||||||
Mediator.Publish(new ClearProfileUserDataMessage());
|
Mediator.Publish(new ClearProfileDataMessage());
|
||||||
_configService.Current.ProfilesShow = showProfiles;
|
_configService.Current.ProfilesShow = showProfiles;
|
||||||
_configService.Save();
|
_configService.Save();
|
||||||
}
|
}
|
||||||
@@ -2345,7 +2321,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.Unindent();
|
ImGui.Unindent();
|
||||||
if (ImGui.Checkbox("Show profiles marked as NSFW", ref showNsfwProfiles))
|
if (ImGui.Checkbox("Show profiles marked as NSFW", ref showNsfwProfiles))
|
||||||
{
|
{
|
||||||
Mediator.Publish(new ClearProfileUserDataMessage());
|
Mediator.Publish(new ClearProfileDataMessage());
|
||||||
_configService.Current.ProfilesAllowNsfw = showNsfwProfiles;
|
_configService.Current.ProfilesAllowNsfw = showNsfwProfiles;
|
||||||
_configService.Save();
|
_configService.Save();
|
||||||
}
|
}
|
||||||
@@ -2355,7 +2331,9 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
|
_uiShared.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
|
||||||
ImGui.TreePop();
|
ImGui.TreePop();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawPerformance()
|
private void DrawPerformance()
|
||||||
@@ -3613,7 +3591,20 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (ImGui.Button($"{FontAwesomeIcon.Play.ToIconString()}##test_pair", new Vector2(availableWidth, 0)))
|
if (ImGui.Button($"{FontAwesomeIcon.Play.ToIconString()}##test_pair", new Vector2(availableWidth, 0)))
|
||||||
{
|
{
|
||||||
Mediator.Publish(new PairRequestReceivedMessage("test-uid-123", "Test User wants to pair with you."));
|
_lightlessNotificationService.ShowPairRequestNotification(
|
||||||
|
"Test User",
|
||||||
|
"test-uid-123",
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
Mediator.Publish(new NotificationMessage("Accepted", "You accepted the test pair request.",
|
||||||
|
NotificationType.Info));
|
||||||
|
},
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
Mediator.Publish(new NotificationMessage("Declined", "You declined the test pair request.",
|
||||||
|
NotificationType.Info));
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("Test pair request notification");
|
UiSharedService.AttachToolTip("Test pair request notification");
|
||||||
@@ -3636,14 +3627,15 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (ImGui.Button($"{FontAwesomeIcon.Play.ToIconString()}##test_download", new Vector2(availableWidth, 0)))
|
if (ImGui.Button($"{FontAwesomeIcon.Play.ToIconString()}##test_download", new Vector2(availableWidth, 0)))
|
||||||
{
|
{
|
||||||
Mediator.Publish(new PairDownloadStatusMessage(
|
_lightlessNotificationService.ShowPairDownloadNotification(
|
||||||
[
|
new List<(string playerName, float progress, string status)>
|
||||||
|
{
|
||||||
("Player One", 0.35f, "downloading"),
|
("Player One", 0.35f, "downloading"),
|
||||||
("Player Two", 0.75f, "downloading"),
|
("Player Two", 0.75f, "downloading"),
|
||||||
("Player Three", 1.0f, "downloading")
|
("Player Three", 1.0f, "downloading")
|
||||||
],
|
},
|
||||||
2
|
queueWaiting: 2
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("Test download progress notification");
|
UiSharedService.AttachToolTip("Test download progress notification");
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
var spacing = ImGui.GetStyle().ItemSpacing;
|
var spacing = ImGui.GetStyle().ItemSpacing;
|
||||||
|
|
||||||
var lightlessProfile = _lightlessProfileManager.GetLightlessUserProfile(Pair.UserData);
|
var lightlessProfile = _lightlessProfileManager.GetLightlessProfile(Pair.UserData);
|
||||||
|
|
||||||
if (_textureWrap == null || !lightlessProfile.ImageData.Value.SequenceEqual(_lastProfilePicture))
|
if (_textureWrap == null || !lightlessProfile.ImageData.Value.SequenceEqual(_lastProfilePicture))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,26 +1,17 @@
|
|||||||
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;
|
||||||
|
|
||||||
@@ -31,51 +22,29 @@ 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, LightlessProfileManager lightlessProfileManager, FileDialogManager fileDialogManager)
|
UiSharedService uiSharedService, PairManager pairManager, GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService)
|
||||||
: 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),
|
||||||
@@ -89,13 +58,10 @@ 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];
|
||||||
|
|
||||||
_profileData = _lightlessProfileManager.GetLightlessGroupProfile(GroupFullInfo.Group);
|
|
||||||
GetTagsFromProfile();
|
|
||||||
|
|
||||||
using var id = ImRaii.PushId("syncshell_admin_" + GroupFullInfo.GID);
|
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"));
|
||||||
|
|
||||||
@@ -103,16 +69,14 @@ 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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,184 +176,6 @@ 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), BannerBase64: null, 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, BannerBase64: 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, BannerBase64: 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, BannerBase64: 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, BannerBase64: 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()
|
||||||
{
|
{
|
||||||
@@ -406,7 +192,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.AliasOrGID, 3, tableFlags);
|
using var table = ImRaii.Table("userList#" + GroupFullInfo.Group.GID, 3, tableFlags);
|
||||||
if (table)
|
if (table)
|
||||||
{
|
{
|
||||||
ImGui.TableSetupColumn("Alias/UID/Note", ImGuiTableColumnFlags.None, 4);
|
ImGui.TableSetupColumn("Alias/UID/Note", ImGuiTableColumnFlags.None, 4);
|
||||||
@@ -688,6 +474,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
}
|
}
|
||||||
mgmtTab.Dispose();
|
mgmtTab.Dispose();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawInvites(GroupPermissions perm)
|
private void DrawInvites(GroupPermissions perm)
|
||||||
@@ -734,37 +521,9 @@ 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, BannerBase64: 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, BannerBase64: 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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,6 +288,8 @@ 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,756 +0,0 @@
|
|||||||
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,7 +1,4 @@
|
|||||||
using System;
|
using System.Security.Cryptography;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace LightlessSync.Utils;
|
namespace LightlessSync.Utils;
|
||||||
@@ -16,9 +13,8 @@ public static class Crypto
|
|||||||
|
|
||||||
public static string GetFileHash(this string filePath)
|
public static string GetFileHash(this string filePath)
|
||||||
{
|
{
|
||||||
using SHA1 sha1 = SHA1.Create();
|
using SHA1CryptoServiceProvider cryptoProvider = new();
|
||||||
using FileStream stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
|
return BitConverter.ToString(cryptoProvider.ComputeHash(File.ReadAllBytes(filePath))).Replace("-", "", StringComparison.Ordinal);
|
||||||
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)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ using Dalamud.Interface.Utility;
|
|||||||
using Lumina.Text;
|
using Lumina.Text;
|
||||||
using System;
|
using System;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Threading;
|
|
||||||
using DalamudSeString = Dalamud.Game.Text.SeStringHandling.SeString;
|
using DalamudSeString = Dalamud.Game.Text.SeStringHandling.SeString;
|
||||||
using DalamudSeStringBuilder = Dalamud.Game.Text.SeStringHandling.SeStringBuilder;
|
using DalamudSeStringBuilder = Dalamud.Game.Text.SeStringHandling.SeStringBuilder;
|
||||||
using LuminaSeStringBuilder = Lumina.Text.SeStringBuilder;
|
using LuminaSeStringBuilder = Lumina.Text.SeStringBuilder;
|
||||||
@@ -16,9 +15,6 @@ namespace LightlessSync.Utils;
|
|||||||
|
|
||||||
public static class SeStringUtils
|
public static class SeStringUtils
|
||||||
{
|
{
|
||||||
private static int _seStringHitboxCounter;
|
|
||||||
private static int _iconHitboxCounter;
|
|
||||||
|
|
||||||
public static DalamudSeString BuildFormattedPlayerName(string text, Vector4? textColor, Vector4? glowColor)
|
public static DalamudSeString BuildFormattedPlayerName(string text, Vector4? textColor, Vector4? glowColor)
|
||||||
{
|
{
|
||||||
var b = new DalamudSeStringBuilder();
|
var b = new DalamudSeStringBuilder();
|
||||||
@@ -123,7 +119,7 @@ public static class SeStringUtils
|
|||||||
|
|
||||||
ImGui.Dummy(new Vector2(0f, textSize.Y));
|
ImGui.Dummy(new Vector2(0f, textSize.Y));
|
||||||
}
|
}
|
||||||
public static Vector2 RenderSeStringWithHitbox(DalamudSeString seString, Vector2 position, ImFontPtr? font = null, string? id = null)
|
public static Vector2 RenderSeStringWithHitbox(DalamudSeString seString, Vector2 position, ImFontPtr? font = null)
|
||||||
{
|
{
|
||||||
var drawList = ImGui.GetWindowDrawList();
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
|
|
||||||
@@ -141,28 +137,12 @@ public static class SeStringUtils
|
|||||||
var textSize = ImGui.CalcTextSize(seString.TextValue);
|
var textSize = ImGui.CalcTextSize(seString.TextValue);
|
||||||
|
|
||||||
ImGui.SetCursorScreenPos(position);
|
ImGui.SetCursorScreenPos(position);
|
||||||
if (id is not null)
|
ImGui.InvisibleButton($"##hitbox_{Guid.NewGuid()}", textSize);
|
||||||
{
|
|
||||||
ImGui.PushID(id);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui.PushID(Interlocked.Increment(ref _seStringHitboxCounter));
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ImGui.InvisibleButton("##hitbox", textSize);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
ImGui.PopID();
|
|
||||||
}
|
|
||||||
|
|
||||||
return textSize;
|
return textSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Vector2 RenderIconWithHitbox(int iconId, Vector2 position, ImFontPtr? font = null, string? id = null)
|
public static Vector2 RenderIconWithHitbox(int iconId, Vector2 position, ImFontPtr? font = null)
|
||||||
{
|
{
|
||||||
var drawList = ImGui.GetWindowDrawList();
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
|
|
||||||
@@ -178,23 +158,7 @@ public static class SeStringUtils
|
|||||||
var drawResult = ImGuiHelpers.CompileSeStringWrapped(iconMacro, drawParams);
|
var drawResult = ImGuiHelpers.CompileSeStringWrapped(iconMacro, drawParams);
|
||||||
|
|
||||||
ImGui.SetCursorScreenPos(position);
|
ImGui.SetCursorScreenPos(position);
|
||||||
if (id is not null)
|
ImGui.InvisibleButton($"##iconHitbox_{Guid.NewGuid()}", drawResult.Size);
|
||||||
{
|
|
||||||
ImGui.PushID(id);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui.PushID(Interlocked.Increment(ref _iconHitboxCounter));
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ImGui.InvisibleButton("##iconHitbox", drawResult.Size);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
ImGui.PopID();
|
|
||||||
}
|
|
||||||
|
|
||||||
return drawResult.Size;
|
return drawResult.Size;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,18 +5,12 @@ 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;
|
||||||
|
|
||||||
@@ -26,27 +20,17 @@ 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,
|
FileCacheManager fileCacheManager, FileCompactor fileCompactor) : base(logger, mediator)
|
||||||
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) =>
|
||||||
{
|
{
|
||||||
@@ -66,11 +50,6 @@ 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)
|
||||||
@@ -177,67 +156,39 @@ 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}", retryCount + 1, requestUrl);
|
Logger.LogDebug("Attempt {attempt} - Downloading {requestUrl} for request {id}", retryCount + 1, requestUrl, requestId);
|
||||||
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}", requestUrl);
|
Logger.LogError($"Max retries reached or cancelled. Failing download for {requestUrl}");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.Delay(retryDelay, ct).ConfigureAwait(false);
|
await Task.Delay(retryDelay, ct).ConfigureAwait(false); // Wait before retrying
|
||||||
}
|
|
||||||
catch (TaskCanceledException ex) when (!ct.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
response?.Dispose();
|
|
||||||
retryCount++;
|
|
||||||
|
|
||||||
Logger.LogWarning(ex, "Cancellation/timeout during download of {requestUrl}. Attempt {attempt} of {maxRetries}", requestUrl, retryCount, maxRetries);
|
|
||||||
|
|
||||||
if (retryCount >= maxRetries)
|
|
||||||
{
|
|
||||||
Logger.LogError("Max retries reached for {requestUrl} after TaskCanceledException", requestUrl);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Delay(retryDelay, ct).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
response?.Dispose();
|
|
||||||
throw;
|
|
||||||
}
|
}
|
||||||
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)
|
||||||
@@ -245,80 +196,42 @@ 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(destinationFilename);
|
fileStream = File.Create(tempPath);
|
||||||
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 with a speed limit of {limit} to {destination}", limit, destinationFilename);
|
Logger.LogTrace("Starting Download of {id} with a speed limit of {limit} to {tempPath}", requestId, limit, tempPath);
|
||||||
|
|
||||||
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 (true)
|
while ((bytesRead = await stream.ReadAsync(buffer, ct).ConfigureAwait(false)) > 0)
|
||||||
{
|
{
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
ct.ThrowIfCancellationRequested();
|
MungeBuffer(buffer.AsSpan(0, bytesRead));
|
||||||
|
|
||||||
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 {destination}", requestUrl, destinationFilename);
|
Logger.LogDebug("{requestUrl} downloaded to {tempPath}", requestUrl, tempPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (TimeoutException ex)
|
|
||||||
{
|
|
||||||
Logger.LogWarning(ex, "Detected stalled download for {requestUrl}, aborting transfer", requestUrl);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
throw;
|
throw;
|
||||||
@@ -327,18 +240,18 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
fileStream?.Close();
|
fileStream?.Close();
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(destinationFilename) && File.Exists(destinationFilename))
|
if (!string.IsNullOrEmpty(tempPath) && File.Exists(tempPath))
|
||||||
{
|
{
|
||||||
File.Delete(destinationFilename);
|
File.Delete(tempPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// ignore cleanup errors
|
// Ignore errors during cleanup
|
||||||
}
|
}
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -347,134 +260,6 @@ 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,76 +307,30 @@ 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 objectName = gameObjectHandler?.Name ?? "Unknown";
|
var downloadGroups = CurrentDownloads.GroupBy(f => f.DownloadUri.Host + ":" + f.DownloadUri.Port, StringComparer.Ordinal);
|
||||||
|
|
||||||
var configAllowsDirect = _configService.Current.EnableDirectDownloads;
|
foreach (var downloadGroup in downloadGroups)
|
||||||
if (configAllowsDirect != _lastConfigDirectDownloadsState)
|
|
||||||
{
|
{
|
||||||
_lastConfigDirectDownloadsState = configAllowsDirect;
|
_downloadStatus[downloadGroup.Key] = new FileDownloadStatus()
|
||||||
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 = directDownload.Total,
|
TotalBytes = downloadGroup.Sum(c => c.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));
|
||||||
|
|
||||||
Task batchDownloadsTask = downloadBatches.Length == 0 ? Task.CompletedTask : Parallel.ForEachAsync(downloadBatches, new ParallelOptions()
|
await Parallel.ForEachAsync(downloadGroups, new ParallelOptions()
|
||||||
{
|
{
|
||||||
MaxDegreeOfParallelism = downloadBatches.Length,
|
MaxDegreeOfParallelism = downloadGroups.Count(),
|
||||||
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,
|
||||||
@@ -614,7 +353,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;
|
||||||
var progress = CreateInlineProgress((bytesDownloaded) =>
|
Progress<long> progress = new((bytesDownloaded) =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -632,7 +371,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("{dlName}: Detected cancellation of download, partially extracting files for {id}", fi.Name, objectName);
|
Logger.LogDebug("{dlName}: Detected cancellation of download, partially extracting files for {id}", fi.Name, gameObjectHandler);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -643,167 +382,72 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
await DecompressBlockFileAsync(fileGroup.Key, blockFile, fileReplacement, fi.Name).ConfigureAwait(false);
|
fileBlockStream = File.OpenRead(blockFile);
|
||||||
|
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);
|
||||||
|
|
||||||
Task directDownloadsTask = directDownloads.Count == 0 ? Task.CompletedTask : Parallel.ForEachAsync(directDownloads, new ParallelOptions()
|
Logger.LogDebug("Download end: {id}", gameObjectHandler);
|
||||||
{
|
|
||||||
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();
|
||||||
}
|
}
|
||||||
@@ -910,24 +554,4 @@ 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,30 +81,27 @@ 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, withToken).ConfigureAwait(false);
|
return await SendRequestInternalAsync(requestMessage, ct, httpCompletionOption).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<HttpResponseMessage> SendRequestAsync<T>(HttpMethod method, Uri uri, T content, CancellationToken ct,
|
public async Task<HttpResponseMessage> SendRequestAsync<T>(HttpMethod method, Uri uri, T content, CancellationToken ct) where T : class
|
||||||
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, withToken: withToken).ConfigureAwait(false);
|
return await SendRequestInternalAsync(requestMessage, ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<HttpResponseMessage> SendRequestStreamAsync(HttpMethod method, Uri uri, ProgressableStreamContent content,
|
public async Task<HttpResponseMessage> SendRequestStreamAsync(HttpMethod method, Uri uri, ProgressableStreamContent content, CancellationToken ct)
|
||||||
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, withToken: withToken).ConfigureAwait(false);
|
return await SendRequestInternalAsync(requestMessage, ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task WaitForDownloadSlotAsync(CancellationToken token)
|
public async Task WaitForDownloadSlotAsync(CancellationToken token)
|
||||||
@@ -147,13 +144,10 @@ 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, bool withToken = true)
|
CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead)
|
||||||
{
|
{
|
||||||
if (withToken)
|
var token = await _tokenProvider.GetToken().ConfigureAwait(false);
|
||||||
{
|
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,7 +18,6 @@ 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;
|
||||||
|
|||||||
@@ -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, BannerPictureBase64: null, Tags: null);
|
if (!IsConnected) return new UserProfileDto(dto.User, Disabled: false, IsNSFW: null, ProfilePictureBase64: null, Description: null);
|
||||||
return await _lightlessHub!.InvokeAsync<UserProfileDto>(nameof(UserGetProfile), dto).ConfigureAwait(false);
|
return await _lightlessHub!.InvokeAsync<UserProfileDto>(nameof(UserGetProfile), dto).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -107,17 +107,10 @@ public partial class ApiController
|
|||||||
}
|
}
|
||||||
public Task Client_ReceiveBroadcastPairRequest(UserPairNotificationDto dto)
|
public Task Client_ReceiveBroadcastPairRequest(UserPairNotificationDto dto)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Client_ReceiveBroadcastPairRequest: {dto}", dto);
|
if (dto == null)
|
||||||
|
|
||||||
if (dto is null)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
|
||||||
|
|
||||||
ExecuteSafely(() =>
|
_pairRequestService.RegisterIncomingRequest(dto.myHashedCid, dto.message ?? string.Empty);
|
||||||
{
|
|
||||||
Mediator.Publish(new PairRequestReceivedMessage(dto.myHashedCid, dto.message ?? string.Empty));
|
|
||||||
});
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
@@ -195,14 +188,7 @@ 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 ClearProfileUserDataMessage(dto.User)));
|
ExecuteSafely(() => Mediator.Publish(new ClearProfileDataMessage(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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,12 +373,6 @@ 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,18 +115,6 @@ 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, BannerBase64: null, 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()
|
||||||
{
|
{
|
||||||
@@ -151,6 +139,7 @@ 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");
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
private readonly ServerConfigurationManager _serverManager;
|
private readonly ServerConfigurationManager _serverManager;
|
||||||
private readonly TokenProvider _tokenProvider;
|
private readonly TokenProvider _tokenProvider;
|
||||||
private readonly LightlessConfigService _lightlessConfigService;
|
private readonly LightlessConfigService _lightlessConfigService;
|
||||||
|
private readonly NotificationService _lightlessNotificationService;
|
||||||
private CancellationTokenSource _connectionCancellationTokenSource;
|
private CancellationTokenSource _connectionCancellationTokenSource;
|
||||||
private ConnectionDto? _connectionDto;
|
private ConnectionDto? _connectionDto;
|
||||||
private bool _doNotNotifyOnNextInfo = false;
|
private bool _doNotNotifyOnNextInfo = false;
|
||||||
@@ -53,6 +54,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
_serverManager = serverManager;
|
_serverManager = serverManager;
|
||||||
_tokenProvider = tokenProvider;
|
_tokenProvider = tokenProvider;
|
||||||
_lightlessConfigService = lightlessConfigService;
|
_lightlessConfigService = lightlessConfigService;
|
||||||
|
_lightlessNotificationService = lightlessNotificationService;
|
||||||
_connectionCancellationTokenSource = new CancellationTokenSource();
|
_connectionCancellationTokenSource = new CancellationTokenSource();
|
||||||
|
|
||||||
Mediator.Subscribe<DalamudLoginMessage>(this, (_) => DalamudUtilOnLogIn());
|
Mediator.Subscribe<DalamudLoginMessage>(this, (_) => DalamudUtilOnLogIn());
|
||||||
@@ -606,7 +608,17 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
ServerState = state;
|
ServerState = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<UserProfileDto?> UserGetLightfinderProfile(string hashedCid)
|
public Task Client_GroupSendProfile(GroupProfileDto groupInfo)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<GroupProfileDto> GroupGetProfile(GroupDto dto)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task GroupSetProfile(GroupProfileDto dto)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,12 +133,6 @@
|
|||||||
"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",
|
||||||
|
|||||||
Reference in New Issue
Block a user