using Dalamud.Bindings.ImGui; using Dalamud.Interface.Colors; using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration.Models; using LightlessSync.PlayerData.Handlers; using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.WebAPI.Files; using LightlessSync.WebAPI.Files.Models; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; using System.Numerics; namespace LightlessSync.UI; public class DownloadUi : WindowMediatorSubscriberBase { private readonly LightlessConfigService _configService; private readonly ConcurrentDictionary> _currentDownloads = new(); private readonly DalamudUtilService _dalamudUtilService; private readonly FileUploadManager _fileTransferManager; private readonly UiSharedService _uiShared; private readonly PairProcessingLimiter _pairProcessingLimiter; private readonly ConcurrentDictionary _uploadingPlayers = new(); private readonly NotificationService _notificationService; private bool _notificationDismissed = true; private int _lastDownloadStateHash = 0; public DownloadUi(ILogger logger, DalamudUtilService dalamudUtilService, LightlessConfigService configService, PairProcessingLimiter pairProcessingLimiter, FileUploadManager fileTransferManager, LightlessMediator mediator, UiSharedService uiShared, PerformanceCollectorService performanceCollectorService, NotificationService notificationService) : base(logger, mediator, "Lightless Sync Downloads", performanceCollectorService) { _dalamudUtilService = dalamudUtilService; _configService = configService; _pairProcessingLimiter = pairProcessingLimiter; _fileTransferManager = fileTransferManager; _uiShared = uiShared; _notificationService = notificationService; SizeConstraints = new WindowSizeConstraints() { MaximumSize = new Vector2(500, 90), MinimumSize = new Vector2(500, 90), }; Flags |= ImGuiWindowFlags.NoMove; Flags |= ImGuiWindowFlags.NoBackground; Flags |= ImGuiWindowFlags.NoInputs; Flags |= ImGuiWindowFlags.NoNavFocus; Flags |= ImGuiWindowFlags.NoResize; Flags |= ImGuiWindowFlags.NoScrollbar; Flags |= ImGuiWindowFlags.NoTitleBar; Flags |= ImGuiWindowFlags.NoDecoration; Flags |= ImGuiWindowFlags.NoFocusOnAppearing; DisableWindowSounds = true; ForceMainWindow = true; IsOpen = true; Mediator.Subscribe(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus); Mediator.Subscribe(this, (msg) => { _currentDownloads.TryRemove(msg.DownloadId, out _); if (!_currentDownloads.Any()) { _notificationService.DismissPairDownloadNotification(); } }); Mediator.Subscribe(this, (_) => IsOpen = false); Mediator.Subscribe(this, (_) => IsOpen = true); Mediator.Subscribe(this, (msg) => { if (msg.IsUploading) { _uploadingPlayers[msg.Handler] = true; } else { _uploadingPlayers.TryRemove(msg.Handler, out _); } }); } protected override void DrawInternal() { if (_configService.Current.ShowTransferWindow) { var limiterSnapshot = _pairProcessingLimiter.GetSnapshot(); try { if (_fileTransferManager.IsUploading) { var currentUploads = _fileTransferManager.GetCurrentUploadsSnapshot(); var totalUploads = currentUploads.Count; var doneUploads = currentUploads.Count(c => c.IsTransferred); var totalUploaded = currentUploads.Sum(c => c.Transferred); var totalToUpload = currentUploads.Sum(c => c.Total); UiSharedService.DrawOutlinedFont($"▲", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); ImGui.SameLine(); var xDistance = ImGui.GetCursorPosX(); UiSharedService.DrawOutlinedFont($"Compressing+Uploading {doneUploads}/{totalUploads}", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); ImGui.NewLine(); ImGui.SameLine(xDistance); UiSharedService.DrawOutlinedFont( $"{UiSharedService.ByteToString(totalUploaded, addSuffix: false)}/{UiSharedService.ByteToString(totalToUpload)}", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); if (_currentDownloads.Any()) ImGui.Separator(); } } catch { // ignore errors thrown from UI } try { // Check if download notifications are enabled (not set to TextOverlay) var useNotifications = _configService.Current.UseLightlessNotifications ? _configService.Current.LightlessDownloadNotification != NotificationLocation.TextOverlay : _configService.Current.UseNotificationsForDownloads; if (useNotifications) { // Use notification system - only update when data actually changes if (_currentDownloads.Any()) { UpdateDownloadNotificationIfChanged(limiterSnapshot); _notificationDismissed = false; } else if (!_notificationDismissed) { _notificationService.DismissPairDownloadNotification(); _notificationDismissed = true; _lastDownloadStateHash = 0; } } else { if (limiterSnapshot.IsEnabled) { var queueColor = limiterSnapshot.Waiting > 0 ? ImGuiColors.DalamudYellow : ImGuiColors.DalamudGrey; var queueText = $"Pair queue {limiterSnapshot.InFlight}/{limiterSnapshot.Limit}"; queueText += limiterSnapshot.Waiting > 0 ? $" ({limiterSnapshot.Waiting} waiting, {limiterSnapshot.Remaining} free)" : $" ({limiterSnapshot.Remaining} free)"; UiSharedService.DrawOutlinedFont(queueText, queueColor, new Vector4(0, 0, 0, 255), 1); ImGui.NewLine(); } else { UiSharedService.DrawOutlinedFont("Pair apply limiter disabled", ImGuiColors.DalamudGrey, new Vector4(0, 0, 0, 255), 1); ImGui.NewLine(); } foreach (var item in _currentDownloads.ToList()) { var dlSlot = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForSlot); var dlQueue = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.WaitingForQueue); var dlProg = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Downloading); var dlDecomp = item.Value.Count(c => c.Value.DownloadStatus == DownloadStatus.Decompressing); var totalFiles = item.Value.Sum(c => c.Value.TotalFiles); var transferredFiles = item.Value.Sum(c => c.Value.TransferredFiles); var totalBytes = item.Value.Sum(c => c.Value.TotalBytes); var transferredBytes = item.Value.Sum(c => c.Value.TransferredBytes); UiSharedService.DrawOutlinedFont($"▼", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); ImGui.SameLine(); var xDistance = ImGui.GetCursorPosX(); UiSharedService.DrawOutlinedFont( $"{item.Key.Name} [W:{dlSlot}/Q:{dlQueue}/P:{dlProg}/D:{dlDecomp}]", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); ImGui.NewLine(); ImGui.SameLine(xDistance); UiSharedService.DrawOutlinedFont( $"{transferredFiles}/{totalFiles} ({UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)})", ImGuiColors.DalamudWhite, new Vector4(0, 0, 0, 255), 1); } } } catch { // ignore errors thrown from UI } } if (_configService.Current.ShowTransferBars) { const int transparency = 100; const int dlBarBorder = 3; foreach (var transfer in _currentDownloads.ToList()) { var screenPos = _dalamudUtilService.WorldToScreen(transfer.Key.GetGameObject()); if (screenPos == Vector2.Zero) continue; var totalBytes = transfer.Value.Sum(c => c.Value.TotalBytes); var transferredBytes = transfer.Value.Sum(c => c.Value.TransferredBytes); var maxDlText = $"{UiSharedService.ByteToString(totalBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}"; var textSize = _configService.Current.TransferBarsShowText ? ImGui.CalcTextSize(maxDlText) : new Vector2(10, 10); int dlBarHeight = _configService.Current.TransferBarsHeight > ((int)textSize.Y + 5) ? _configService.Current.TransferBarsHeight : (int)textSize.Y + 5; int dlBarWidth = _configService.Current.TransferBarsWidth > ((int)textSize.X + 10) ? _configService.Current.TransferBarsWidth : (int)textSize.X + 10; var dlBarStart = new Vector2(screenPos.X - dlBarWidth / 2f, screenPos.Y - dlBarHeight / 2f); var dlBarEnd = new Vector2(screenPos.X + dlBarWidth / 2f, screenPos.Y + dlBarHeight / 2f); var drawList = ImGui.GetBackgroundDrawList(); drawList.AddRectFilled( dlBarStart with { X = dlBarStart.X - dlBarBorder - 1, Y = dlBarStart.Y - dlBarBorder - 1 }, dlBarEnd with { X = dlBarEnd.X + dlBarBorder + 1, Y = dlBarEnd.Y + dlBarBorder + 1 }, UiSharedService.Color(0, 0, 0, transparency), 1); drawList.AddRectFilled(dlBarStart with { X = dlBarStart.X - dlBarBorder, Y = dlBarStart.Y - dlBarBorder }, dlBarEnd with { X = dlBarEnd.X + dlBarBorder, Y = dlBarEnd.Y + dlBarBorder }, UiSharedService.Color(220, 220, 220, transparency), 1); drawList.AddRectFilled(dlBarStart, dlBarEnd, UiSharedService.Color(0, 0, 0, transparency), 1); var dlProgressPercent = transferredBytes / (double)totalBytes; drawList.AddRectFilled(dlBarStart, dlBarEnd with { X = dlBarStart.X + (float)(dlProgressPercent * dlBarWidth) }, UiSharedService.Color(UIColors.Get("LightlessPurple"))); if (_configService.Current.TransferBarsShowText) { var downloadText = $"{UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}"; UiSharedService.DrawOutlinedFont(drawList, downloadText, screenPos with { X = screenPos.X - textSize.X / 2f - 1, Y = screenPos.Y - textSize.Y / 2f - 1 }, UiSharedService.Color(255, 255, 255, transparency), UiSharedService.Color(0, 0, 0, transparency), 1); } } if (_configService.Current.ShowUploading) { foreach (var player in _uploadingPlayers.Select(p => p.Key).ToList()) { var screenPos = _dalamudUtilService.WorldToScreen(player.GetGameObject()); if (screenPos == Vector2.Zero) continue; try { using var _ = _uiShared.UidFont.Push(); var uploadText = "Uploading"; var textSize = ImGui.CalcTextSize(uploadText); var drawList = ImGui.GetBackgroundDrawList(); UiSharedService.DrawOutlinedFont(drawList, uploadText, screenPos with { X = screenPos.X - textSize.X / 2f - 1, Y = screenPos.Y - textSize.Y / 2f - 1 }, UiSharedService.Color(255, 255, 0, transparency), UiSharedService.Color(0, 0, 0, transparency), 2); } catch { // ignore errors thrown on UI } } } } } public override bool DrawConditions() { if (_uiShared.EditTrackerPosition) return true; if (!_configService.Current.ShowTransferWindow && !_configService.Current.ShowTransferBars) return false; if (!_currentDownloads.Any() && !_fileTransferManager.IsUploading && !_uploadingPlayers.Any()) return false; if (!IsOpen) return false; return true; } public override void PreDraw() { base.PreDraw(); if (_uiShared.EditTrackerPosition) { Flags &= ~ImGuiWindowFlags.NoMove; Flags &= ~ImGuiWindowFlags.NoBackground; Flags &= ~ImGuiWindowFlags.NoInputs; Flags &= ~ImGuiWindowFlags.NoResize; } else { Flags |= ImGuiWindowFlags.NoMove; Flags |= ImGuiWindowFlags.NoBackground; Flags |= ImGuiWindowFlags.NoInputs; Flags |= ImGuiWindowFlags.NoResize; } var maxHeight = ImGui.GetTextLineHeight() * (_configService.Current.ParallelDownloads + 3); SizeConstraints = new() { MinimumSize = new Vector2(300, maxHeight), MaximumSize = new Vector2(300, maxHeight), }; } private void UpdateDownloadNotificationIfChanged(PairProcessingLimiterSnapshot limiterSnapshot) { var downloadStatus = new List<(string playerName, float progress, string status)>(_currentDownloads.Count); var hashCode = new HashCode(); foreach (var item in _currentDownloads) { var dlSlot = 0; var dlQueue = 0; var dlProg = 0; var dlDecomp = 0; long totalBytes = 0; long transferredBytes = 0; // Single pass through the dictionary to count everything - avoid multiple LINQ iterations foreach (var entry in item.Value) { var fileStatus = entry.Value; switch (fileStatus.DownloadStatus) { case DownloadStatus.WaitingForSlot: dlSlot++; break; case DownloadStatus.WaitingForQueue: dlQueue++; break; case DownloadStatus.Downloading: dlProg++; break; case DownloadStatus.Decompressing: dlDecomp++; break; } totalBytes += fileStatus.TotalBytes; transferredBytes += fileStatus.TransferredBytes; } var progress = totalBytes > 0 ? (float)transferredBytes / totalBytes : 0f; string status; if (dlDecomp > 0) status = "decompressing"; else if (dlProg > 0) status = "downloading"; else if (dlQueue > 0) status = "queued"; else if (dlSlot > 0) status = "waiting"; else status = "completed"; downloadStatus.Add((item.Key.Name, progress, status)); // Build hash from meaningful state hashCode.Add(item.Key.Name); hashCode.Add(transferredBytes); hashCode.Add(totalBytes); hashCode.Add(status); } var queueWaiting = limiterSnapshot.IsEnabled ? limiterSnapshot.Waiting : 0; hashCode.Add(queueWaiting); var currentHash = hashCode.ToHashCode(); // Only update notification if state has actually changed if (currentHash != _lastDownloadStateHash) { _lastDownloadStateHash = currentHash; if (downloadStatus.Count > 0 || queueWaiting > 0) { _notificationService.ShowPairDownloadNotification(downloadStatus, queueWaiting); } } } }