Files
LightlessClient/LightlessSync/UI/DownloadUi.cs

889 lines
32 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 = [];
private byte _transferBoxTransparency = 100;
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)
{
var limiterSnapshot = _pairProcessingLimiter.GetSnapshot();
// 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)
{
if (!_currentDownloads.IsEmpty)
{
UpdateDownloadNotificationIfChanged(limiterSnapshot);
_notificationDismissed = false;
}
else if (!_notificationDismissed)
{
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
_notificationDismissed = true;
_lastDownloadStateHash = 0;
}
}
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)
{
DrawTransferBar();
}
}
private void DrawTransferBar()
{
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);
// Per-player state counts
var dlSlot = 0;
var dlQueue = 0;
var dlProg = 0;
var dlDecomp = 0;
foreach (var entry in transfer.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;
}
}
string statusText;
if (dlProg > 0)
{
statusText = "Downloading";
}
else if (dlDecomp > 0 || (totalBytes > 0 && transferredBytes >= totalBytes))
{
statusText = "Decompressing";
}
else if (dlQueue > 0)
{
statusText = "Waiting for queue";
}
else if (dlSlot > 0)
{
statusText = "Waiting for slot";
}
else
{
statusText = "Waiting";
}
var hasValidSize = totalBytes > 0;
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();
drawList.AddRectFilled(
outerStart + shadowOffset,
outerEnd + shadowOffset,
UiSharedService.Color(0, 0, 0, 100 / 2),
rounding + 2
);
drawList.AddRectFilled(
outerStart,
outerEnd,
UiSharedService.Color(0, 0, 0, 100),
rounding + 2
);
drawList.AddRectFilled(
borderStart,
borderEnd,
UiSharedService.Color(ImGuiColors.DalamudGrey),
rounding
);
drawList.AddRectFilled(
dlBarStart,
dlBarEnd,
UiSharedService.Color(0, 0, 0, 100),
rounding
);
bool showFill = false;
double fillPercent = 0.0;
if (hasValidSize)
{
if (dlProg > 0)
{
fillPercent = transferredBytes / (double)totalBytes;
showFill = true;
}
else if (dlDecomp > 0 || transferredBytes >= totalBytes)
{
fillPercent = 1.0;
showFill = true;
}
}
if (showFill)
{
if (fillPercent < 0) fillPercent = 0;
if (fillPercent > 1) fillPercent = 1;
var progressEndX = dlBarStart.X + (float)(fillPercent * dlBarWidth);
var progressEnd = new Vector2(progressEndX, dlBarEnd.Y);
drawList.AddRectFilled(
dlBarStart,
progressEnd,
UiSharedService.Color(UIColors.Get("LightlessPurple")),
rounding
);
}
if (_configService.Current.TransferBarsShowText)
{
string downloadText;
if (dlProg > 0 && hasValidSize)
{
downloadText =
$"{statusText} {UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
}
else if ((dlDecomp > 0 || transferredBytes >= totalBytes) && hasValidSize)
{
downloadText = "Decompressing";
}
else
{
// Waiting states
downloadText = statusText;
}
var actualTextSize = ImGui.CalcTextSize(downloadText);
UiSharedService.DrawOutlinedFont(
drawList,
downloadText,
screenPos with
{
X = screenPos.X - actualTextSize.X / 2f - 1,
Y = screenPos.Y - actualTextSize.Y / 2f - 1
},
UiSharedService.Color(ImGuiColors.DalamudGrey),
UiSharedService.Color(0, 0, 0, 100),
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(ImGuiColors.DalamudYellow),
UiSharedService.Color(0, 0, 0, 100),
2
);
}
catch
{
_logger.LogDebug("Error drawing upload progress");
}
}
}
}
private void DrawDownloadSummaryBox()
{
if (_currentDownloads.IsEmpty)
return;
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 totalDlSlot = 0;
var totalDlQueue = 0;
var totalDlProg = 0;
var totalDlDecomp = 0;
var perPlayer = new List<(
string Name,
int TransferredFiles,
int TotalFiles,
long TransferredBytes,
long TotalBytes,
double SpeedBytesPerSecond,
int DlSlot,
int DlQueue,
int DlProg,
int DlDecomp)>();
foreach (var transfer in _currentDownloads)
{
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;
// per-player W/Q/P/D
var playerDlSlot = 0;
var playerDlQueue = 0;
var playerDlProg = 0;
var playerDlDecomp = 0;
foreach (var entry in transfer.Value)
{
var fileStatus = entry.Value;
switch (fileStatus.DownloadStatus)
{
case DownloadStatus.WaitingForSlot:
playerDlSlot++;
totalDlSlot++;
break;
case DownloadStatus.WaitingForQueue:
playerDlQueue++;
totalDlQueue++;
break;
case DownloadStatus.Downloading:
playerDlProg++;
totalDlProg++;
break;
case DownloadStatus.Decompressing:
playerDlDecomp++;
totalDlDecomp++;
break;
}
}
double speed = 0;
if (playerTotalBytes > 0)
{
if (!_downloadSpeeds.TryGetValue(handler, out var tracker))
{
tracker = new DownloadSpeedTracker(windowSeconds: 3.0);
_downloadSpeeds[handler] = tracker;
}
speed = tracker.Update(now, playerTransferredBytes);
}
perPlayer.Add((
handler.Name,
playerTransferredFiles,
playerTotalFiles,
playerTransferredBytes,
playerTotalBytes,
speed,
playerDlSlot,
playerDlQueue,
playerDlProg,
playerDlDecomp
));
}
// Clean speed trackers for players with no active downloads
foreach (var handler in _downloadSpeeds.Keys.ToList())
{
if (!_currentDownloads.ContainsKey(handler))
_downloadSpeeds.Remove(handler);
}
if (totalFiles == 0 || totalBytes == 0)
return;
// max speed for per-player bar scale (clamped)
double maxSpeed = perPlayer.Count > 0 ? perPlayer.Max(p => p.SpeedBytesPerSecond) : 0;
if (maxSpeed <= 0)
maxSpeed = 1;
var drawList = ImGui.GetBackgroundDrawList();
var windowPos = ImGui.GetWindowPos();
// Overall texts
var headerText =
$"Downloading {transferredFiles}/{totalFiles} files [W:{totalDlSlot}/Q:{totalDlQueue}/P:{totalDlProg}/D:{totalDlDecomp}]";
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 in lightspeed...";
var headerSize = ImGui.CalcTextSize(headerText);
var bytesSize = ImGui.CalcTextSize(bytesText);
var totalSpeedSize = ImGui.CalcTextSize(speedText);
float contentWidth = headerSize.X;
if (bytesSize.X > contentWidth) contentWidth = bytesSize.X;
if (totalSpeedSize.X > contentWidth) contentWidth = totalSpeedSize.X;
if (_configService.Current.ShowPlayerLinesTransferWindow)
{
foreach (var p in perPlayer)
{
var line =
$"{p.Name} [W:{p.DlSlot}/Q:{p.DlQueue}/P:{p.DlProg}/D:{p.DlDecomp}] {p.TransferredFiles}/{p.TotalFiles}";
var lineSize = ImGui.CalcTextSize(line);
if (lineSize.X > contentWidth)
contentWidth = lineSize.X;
}
}
var lineHeight = ImGui.GetTextLineHeight();
var globalBarHeight = lineHeight * 0.8f;
var perPlayerBarHeight = lineHeight * 0.4f;
// Box width
float boxWidth = contentWidth + padding * 2;
if (boxWidth < minBoxWidth)
boxWidth = minBoxWidth;
// Box height
float boxHeight = 0;
boxHeight += padding;
boxHeight += globalBarHeight;
boxHeight += padding;
boxHeight += lineHeight + spacingY;
boxHeight += lineHeight + spacingY;
boxHeight += lineHeight * 1.4f + spacingY;
if (_configService.Current.ShowPlayerLinesTransferWindow)
{
foreach (var p in perPlayer)
{
boxHeight += lineHeight + spacingY;
if (_configService.Current.ShowPlayerSpeedBarsTransferWindow && p.DlProg > 0)
{
boxHeight += perPlayerBarHeight + spacingY;
}
}
}
boxHeight += padding;
var boxMin = windowPos;
var boxMax = new Vector2(windowPos.X + boxWidth, windowPos.Y + boxHeight);
// Background + border
drawList.AddRectFilled(boxMin, boxMax, UiSharedService.Color(0, 0, 0, _transferBoxTransparency), 5f);
drawList.AddRect(boxMin, boxMax, UiSharedService.Color(ImGuiColors.DalamudGrey), 5f);
var cursor = boxMin + new Vector2(padding, padding);
var barMin = cursor;
var barMax = new Vector2(boxMin.X + boxWidth - padding, cursor.Y + globalBarHeight);
var progress = (float)transferredBytes / totalBytes;
if (progress < 0f) progress = 0f;
if (progress > 1f) progress = 1f;
drawList.AddRectFilled(barMin, barMax, UiSharedService.Color(40, 40, 40, _transferBoxTransparency), 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(ImGuiColors.DalamudWhite),
UiSharedService.Color(0, 0, 0, _transferBoxTransparency),
1
);
cursor.Y += lineHeight + spacingY;
// Bytes
UiSharedService.DrawOutlinedFont(
drawList,
bytesText,
cursor,
UiSharedService.Color(ImGuiColors.DalamudWhite),
UiSharedService.Color(0, 0, 0, _transferBoxTransparency),
1
);
cursor.Y += lineHeight + spacingY;
// Total speed
UiSharedService.DrawOutlinedFont(
drawList,
speedText,
cursor,
UiSharedService.Color(UIColors.Get("LightlessPurple")),
UiSharedService.Color(0, 0, 0, _transferBoxTransparency),
1
);
cursor.Y += lineHeight * 1.4f + spacingY;
if (_configService.Current.ShowPlayerLinesTransferWindow)
{
var orderedPlayers = perPlayer.OrderByDescending(p => p.TotalBytes).ToList();
foreach (var p in orderedPlayers)
{
var playerSpeedText = p.SpeedBytesPerSecond > 0
? $"{UiSharedService.ByteToString((long)p.SpeedBytesPerSecond)}/s"
: "-";
var labelLine =
$"{p.Name} [W:{p.DlSlot}/Q:{p.DlQueue}/P:{p.DlProg}/D:{p.DlDecomp}] {p.TransferredFiles}/{p.TotalFiles}";
if (!_configService.Current.ShowPlayerSpeedBarsTransferWindow || p.DlProg <= 0)
{
var fullLine =
$"{labelLine} " +
$"({UiSharedService.ByteToString(p.TransferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(p.TotalBytes)}) " +
$"@ {playerSpeedText}";
UiSharedService.DrawOutlinedFont(
drawList,
fullLine,
cursor,
UiSharedService.Color(255, 255, 255, _transferBoxTransparency),
UiSharedService.Color(0, 0, 0, _transferBoxTransparency),
1
);
cursor.Y += lineHeight + spacingY;
continue;
}
UiSharedService.DrawOutlinedFont(
drawList,
labelLine,
cursor,
UiSharedService.Color(255, 255, 255, _transferBoxTransparency),
UiSharedService.Color(0, 0, 0, _transferBoxTransparency),
1
);
cursor.Y += lineHeight + spacingY;
var barBgMin = new Vector2(boxMin.X + padding, cursor.Y);
var barBgMax = new Vector2(boxMax.X - padding, cursor.Y + perPlayerBarHeight);
drawList.AddRectFilled(
barBgMin,
barBgMax,
UiSharedService.Color(40, 40, 40, _transferBoxTransparency),
3f
);
float ratio = 0f;
if (maxSpeed > 0)
ratio = (float)(p.SpeedBytesPerSecond / maxSpeed);
if (ratio < 0f) ratio = 0f;
if (ratio > 1f) ratio = 1f;
var fillX = barBgMin.X + (barBgMax.X - barBgMin.X) * ratio;
var barFillMax = new Vector2(fillX, barBgMax.Y);
drawList.AddRectFilled(
barBgMin,
barFillMax,
UiSharedService.Color(UIColors.Get("LightlessPurple")),
3f
);
var barText =
$"{UiSharedService.ByteToString(p.TransferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(p.TotalBytes)} @ {playerSpeedText}";
var barTextSize = ImGui.CalcTextSize(barText);
var barTextPos = new Vector2(
barBgMin.X + ((barBgMax.X - barBgMin.X) - barTextSize.X) / 2f - 1,
barBgMin.Y + ((perPlayerBarHeight - barTextSize.Y) / 2f) - 1
);
UiSharedService.DrawOutlinedFont(
drawList,
barText,
barTextPos,
UiSharedService.Color(255, 255, 255, _transferBoxTransparency),
UiSharedService.Color(0, 0, 0, _transferBoxTransparency),
1
);
cursor.Y += perPlayerBarHeight + spacingY;
}
}
}
public override bool DrawConditions()
{
if (_uiShared.EditTrackerPosition) return true;
if (!_configService.Current.ShowTransferWindow && !_configService.Current.ShowTransferBars) return false;
if (_currentDownloads.IsEmpty && !_fileTransferManager.IsUploading && _uploadingPlayers.IsEmpty) 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));
}
}
}
private sealed class DownloadSpeedTracker
{
private readonly Queue<(double Time, long Bytes)> _samples = new();
private readonly double _windowSeconds;
public double SpeedBytesPerSecond { get; private set; }
public DownloadSpeedTracker(double windowSeconds = 3.0)
{
_windowSeconds = windowSeconds;
}
public double Update(double now, long totalBytes)
{
if (_samples.Count > 0 && totalBytes < _samples.Last().Bytes)
{
_samples.Clear();
}
_samples.Enqueue((now, totalBytes));
while (_samples.Count > 0 && now - _samples.Peek().Time > _windowSeconds)
_samples.Dequeue();
if (_samples.Count < 2)
{
SpeedBytesPerSecond = 0;
return SpeedBytesPerSecond;
}
var oldest = _samples.Peek();
var newest = _samples.Last();
var dt = newest.Time - oldest.Time;
if (dt <= 0.0001)
{
SpeedBytesPerSecond = 0;
return SpeedBytesPerSecond;
}
var dBytes = newest.Bytes - oldest.Bytes;
if (dBytes <= 0)
{
SpeedBytesPerSecond = 0;
return SpeedBytesPerSecond;
}
const long minBytesForSpeed = 32 * 1024;
if (dBytes < minBytesForSpeed)
{
return SpeedBytesPerSecond;
}
var avg = dBytes / dt;
const double alpha = 0.3;
SpeedBytesPerSecond = SpeedBytesPerSecond <= 0
? avg
: SpeedBytesPerSecond * (1 - alpha) + avg * alpha;
return SpeedBytesPerSecond;
}
}
}