534 lines
22 KiB
C#
534 lines
22 KiB
C#
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.Services.PairProcessing;
|
|
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<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
|
|
private readonly DalamudUtilService _dalamudUtilService;
|
|
private readonly FileUploadManager _fileTransferManager;
|
|
private readonly UiSharedService _uiShared;
|
|
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
|
private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new();
|
|
private readonly Dictionary<GameObjectHandler, Vector2> _smoothed = [];
|
|
private readonly Dictionary<GameObjectHandler, DownloadSpeedTracker> _downloadSpeeds = new();
|
|
|
|
private sealed class DownloadSpeedTracker
|
|
{
|
|
public long LastBytes;
|
|
public double LastTime;
|
|
public double SpeedBytesPerSecond;
|
|
}
|
|
|
|
private bool _notificationDismissed = true;
|
|
private int _lastDownloadStateHash = 0;
|
|
|
|
public DownloadUi(ILogger<DownloadUi> logger, DalamudUtilService dalamudUtilService, LightlessConfigService configService,
|
|
PairProcessingLimiter pairProcessingLimiter, FileUploadManager fileTransferManager, LightlessMediator mediator, UiSharedService uiShared,
|
|
PerformanceCollectorService performanceCollectorService)
|
|
: base(logger, mediator, "Lightless Sync Downloads", performanceCollectorService)
|
|
{
|
|
_dalamudUtilService = dalamudUtilService;
|
|
_configService = configService;
|
|
_pairProcessingLimiter = pairProcessingLimiter;
|
|
_fileTransferManager = fileTransferManager;
|
|
_uiShared = uiShared;
|
|
|
|
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<DownloadStartedMessage>(this, (msg) =>
|
|
{
|
|
_currentDownloads[msg.DownloadId] = msg.DownloadStatus;
|
|
_notificationDismissed = false;
|
|
});
|
|
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) =>
|
|
{
|
|
_currentDownloads.TryRemove(msg.DownloadId, out _);
|
|
|
|
// Dismiss notification if all downloads are complete
|
|
if (!_currentDownloads.Any() && !_notificationDismissed)
|
|
{
|
|
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
|
|
_notificationDismissed = true;
|
|
_lastDownloadStateHash = 0;
|
|
}
|
|
});
|
|
Mediator.Subscribe<GposeStartMessage>(this, (_) => IsOpen = false);
|
|
Mediator.Subscribe<GposeEndMessage>(this, (_) => IsOpen = true);
|
|
Mediator.Subscribe<PlayerUploadingMessage>(this, (msg) =>
|
|
{
|
|
if (msg.IsUploading)
|
|
{
|
|
_uploadingPlayers[msg.Handler] = true;
|
|
}
|
|
else
|
|
{
|
|
_uploadingPlayers.TryRemove(msg.Handler, out _);
|
|
}
|
|
});
|
|
}
|
|
|
|
protected override void DrawInternal()
|
|
{
|
|
if (_configService.Current.ShowTransferWindow)
|
|
{
|
|
DrawDownloadSummaryBox();
|
|
|
|
if (_configService.Current.ShowUploading)
|
|
{
|
|
const int transparency = 100;
|
|
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
|
|
{
|
|
_logger.LogDebug("Error drawing upload progress");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (_configService.Current.ShowTransferBars)
|
|
{
|
|
const int transparency = 100;
|
|
const int dlBarBorder = 3;
|
|
const float rounding = 6f;
|
|
var shadowOffset = new Vector2(2, 2);
|
|
|
|
foreach (var transfer in _currentDownloads.ToList())
|
|
{
|
|
var transferKey = transfer.Key;
|
|
var rawPos = _dalamudUtilService.WorldToScreen(transferKey.GetGameObject());
|
|
|
|
//If RawPos is zero, remove it from smoothed dictionary
|
|
if (rawPos == Vector2.Zero)
|
|
{
|
|
_smoothed.Remove(transferKey);
|
|
continue;
|
|
}
|
|
//Smoothing out the movement and fix jitter around the position.
|
|
|
|
Vector2 screenPos = _smoothed.TryGetValue(transferKey, out var lastPos)
|
|
? (rawPos - lastPos).Length() < 4f ? lastPos : rawPos
|
|
: rawPos;
|
|
_smoothed[transferKey] = screenPos;
|
|
|
|
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);
|
|
|
|
// Precompute rects
|
|
var outerStart = new Vector2(dlBarStart.X - dlBarBorder - 1, dlBarStart.Y - dlBarBorder - 1);
|
|
var outerEnd = new Vector2(dlBarEnd.X + dlBarBorder + 1, dlBarEnd.Y + dlBarBorder + 1);
|
|
var borderStart = new Vector2(dlBarStart.X - dlBarBorder, dlBarStart.Y - dlBarBorder);
|
|
var borderEnd = new Vector2(dlBarEnd.X + dlBarBorder, dlBarEnd.Y + dlBarBorder);
|
|
|
|
var drawList = ImGui.GetBackgroundDrawList();
|
|
|
|
//Shadow, background, border, bar background
|
|
drawList.AddRectFilled(outerStart + shadowOffset, outerEnd + shadowOffset, UiSharedService.Color(0, 0, 0, transparency / 2), rounding + 2);
|
|
drawList.AddRectFilled(outerStart, outerEnd, UiSharedService.Color(0, 0, 0, transparency), rounding + 2);
|
|
drawList.AddRectFilled(borderStart, borderEnd, UiSharedService.Color(220, 220, 220, transparency), rounding);
|
|
drawList.AddRectFilled(dlBarStart, dlBarEnd, UiSharedService.Color(0, 0, 0, transparency), rounding);
|
|
|
|
var dlProgressPercent = transferredBytes / (double)totalBytes;
|
|
var progressEndX = dlBarStart.X + (float)(dlProgressPercent * dlBarWidth);
|
|
var progressEnd = new Vector2(progressEndX, dlBarEnd.Y);
|
|
|
|
drawList.AddRectFilled(
|
|
dlBarStart,
|
|
progressEnd,
|
|
UiSharedService.Color(UIColors.Get("LightlessPurple")),
|
|
rounding
|
|
);
|
|
|
|
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
|
|
{
|
|
_logger.LogDebug("Error drawing upload progress");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DrawDownloadSummaryBox()
|
|
{
|
|
if (!_currentDownloads.Any())
|
|
return;
|
|
|
|
const int transparency = 150;
|
|
const float padding = 6f;
|
|
const float spacingY = 2f;
|
|
const float minBoxWidth = 320f;
|
|
|
|
var now = ImGui.GetTime();
|
|
|
|
int totalFiles = 0;
|
|
int transferredFiles = 0;
|
|
long totalBytes = 0;
|
|
long transferredBytes = 0;
|
|
|
|
var perPlayer = new List<(string Name, int TransferredFiles, int TotalFiles, long TransferredBytes, long TotalBytes, double SpeedBytesPerSecond)>();
|
|
|
|
foreach (var transfer in _currentDownloads.ToList())
|
|
{
|
|
var handler = transfer.Key;
|
|
var statuses = transfer.Value.Values;
|
|
|
|
var playerTotalFiles = statuses.Sum(s => s.TotalFiles);
|
|
var playerTransferredFiles = statuses.Sum(s => s.TransferredFiles);
|
|
var playerTotalBytes = statuses.Sum(s => s.TotalBytes);
|
|
var playerTransferredBytes = statuses.Sum(s => s.TransferredBytes);
|
|
|
|
totalFiles += playerTotalFiles;
|
|
transferredFiles += playerTransferredFiles;
|
|
totalBytes += playerTotalBytes;
|
|
transferredBytes += playerTransferredBytes;
|
|
|
|
double speed = 0;
|
|
if (playerTotalBytes > 0)
|
|
{
|
|
if (!_downloadSpeeds.TryGetValue(handler, out var tracker))
|
|
{
|
|
tracker = new DownloadSpeedTracker
|
|
{
|
|
LastBytes = playerTransferredBytes,
|
|
LastTime = now,
|
|
SpeedBytesPerSecond = 0
|
|
};
|
|
_downloadSpeeds[handler] = tracker;
|
|
}
|
|
|
|
var dt = now - tracker.LastTime;
|
|
var dBytes = playerTransferredBytes - tracker.LastBytes;
|
|
|
|
if (dt > 0.1 && dBytes >= 0)
|
|
{
|
|
var instant = dBytes / dt;
|
|
tracker.SpeedBytesPerSecond = tracker.SpeedBytesPerSecond <= 0
|
|
? instant
|
|
: tracker.SpeedBytesPerSecond * 0.8 + instant * 0.2;
|
|
}
|
|
|
|
tracker.LastTime = now;
|
|
tracker.LastBytes = playerTransferredBytes;
|
|
speed = tracker.SpeedBytesPerSecond;
|
|
}
|
|
|
|
perPlayer.Add((
|
|
handler.Name,
|
|
playerTransferredFiles,
|
|
playerTotalFiles,
|
|
playerTransferredBytes,
|
|
playerTotalBytes,
|
|
speed
|
|
));
|
|
}
|
|
|
|
foreach (var handler in _downloadSpeeds.Keys.ToList())
|
|
{
|
|
if (!_currentDownloads.ContainsKey(handler))
|
|
_downloadSpeeds.Remove(handler);
|
|
}
|
|
|
|
if (totalFiles == 0 || totalBytes == 0)
|
|
return;
|
|
|
|
var drawList = ImGui.GetBackgroundDrawList();
|
|
var windowPos = ImGui.GetWindowPos();
|
|
|
|
var headerText = $"Downloading {transferredFiles}/{totalFiles} files";
|
|
var bytesText = $"{UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
|
|
|
|
var totalSpeed = perPlayer.Sum(p => p.SpeedBytesPerSecond);
|
|
var speedText = totalSpeed > 0
|
|
? $"{UiSharedService.ByteToString((long)totalSpeed)}/s"
|
|
: "Calculating lightspeed...";
|
|
|
|
var headerSize = ImGui.CalcTextSize(headerText);
|
|
var bytesSize = ImGui.CalcTextSize(bytesText);
|
|
var speedSize = ImGui.CalcTextSize(speedText);
|
|
|
|
float contentWidth = headerSize.X;
|
|
if (bytesSize.X > contentWidth) contentWidth = bytesSize.X;
|
|
if (speedSize.X > contentWidth) contentWidth = speedSize.X;
|
|
|
|
foreach (var p in perPlayer)
|
|
{
|
|
var playerSpeedText = p.SpeedBytesPerSecond > 0
|
|
? $"{UiSharedService.ByteToString((long)p.SpeedBytesPerSecond)}/s"
|
|
: "-";
|
|
|
|
var line = $"{p.Name}: {p.TransferredFiles}/{p.TotalFiles} " +
|
|
$"({UiSharedService.ByteToString(p.TransferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(p.TotalBytes)}) " +
|
|
$"@ {playerSpeedText}";
|
|
|
|
var lineSize = ImGui.CalcTextSize(line);
|
|
if (lineSize.X > contentWidth)
|
|
contentWidth = lineSize.X;
|
|
}
|
|
|
|
var boxWidth = contentWidth + padding * 2;
|
|
if (boxWidth < minBoxWidth)
|
|
boxWidth = minBoxWidth;
|
|
|
|
var lineHeight = ImGui.GetTextLineHeight();
|
|
var numTextLines = 3 + perPlayer.Count;
|
|
var barHeight = lineHeight * 0.8f;
|
|
var boxHeight = padding * 3 + barHeight + numTextLines * (lineHeight + spacingY);
|
|
|
|
var origin = windowPos;
|
|
|
|
var boxMin = origin;
|
|
var boxMax = origin + new Vector2(boxWidth, boxHeight);
|
|
|
|
drawList.AddRectFilled(boxMin, boxMax, UiSharedService.Color(0, 0, 0, transparency), 5f);
|
|
drawList.AddRect(boxMin, boxMax, UiSharedService.Color(220, 220, 220, transparency), 5f);
|
|
|
|
// Progress bar
|
|
var cursor = boxMin + new Vector2(padding, padding);
|
|
var barMin = cursor;
|
|
var barMax = new Vector2(boxMin.X + boxWidth - padding, cursor.Y + barHeight);
|
|
|
|
var progress = (float)transferredBytes / totalBytes;
|
|
drawList.AddRectFilled(barMin, barMax, UiSharedService.Color(40, 40, 40, transparency), 3f);
|
|
drawList.AddRectFilled(
|
|
barMin,
|
|
new Vector2(barMin.X + (barMax.X - barMin.X) * progress, barMax.Y),
|
|
UiSharedService.Color(UIColors.Get("LightlessPurple")),
|
|
3f
|
|
);
|
|
|
|
cursor.Y = barMax.Y + padding;
|
|
|
|
// Header
|
|
UiSharedService.DrawOutlinedFont(drawList, headerText, cursor, UiSharedService.Color(255, 255, 255, transparency), UiSharedService.Color(0, 0, 0, transparency), 1);
|
|
cursor.Y += lineHeight + spacingY;
|
|
|
|
// Bytes
|
|
UiSharedService.DrawOutlinedFont(drawList, bytesText, cursor, UiSharedService.Color(255, 255, 255, transparency), UiSharedService.Color(0, 0, 0, transparency), 1);
|
|
cursor.Y += lineHeight + spacingY;
|
|
|
|
// Total speed WIP
|
|
UiSharedService.DrawOutlinedFont(drawList, speedText, cursor, UiSharedService.Color(200, 255, 200, transparency), UiSharedService.Color(0, 0, 0, transparency), 1);
|
|
cursor.Y += lineHeight * 1.4f;
|
|
|
|
// Per-player lines
|
|
foreach (var p in perPlayer.OrderByDescending(p => p.TotalBytes))
|
|
{
|
|
var playerSpeedText = p.SpeedBytesPerSecond > 0
|
|
? $"{UiSharedService.ByteToString((long)p.SpeedBytesPerSecond)}/s"
|
|
: "-";
|
|
|
|
var line = $"{p.Name}: {p.TransferredFiles}/{p.TotalFiles} " +
|
|
$"({UiSharedService.ByteToString(p.TransferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(p.TotalBytes)}) " +
|
|
$"@ {playerSpeedText}";
|
|
|
|
UiSharedService.DrawOutlinedFont(drawList, line, cursor, UiSharedService.Color(255, 255, 255, transparency), UiSharedService.Color(0, 0, 0, transparency), 1);
|
|
|
|
cursor.Y += lineHeight + spacingY;
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
Mediator.Publish(new PairDownloadStatusMessage(downloadStatus, queueWaiting));
|
|
}
|
|
}
|
|
}
|
|
} |