-
Notifications
You must be signed in to change notification settings - Fork 47
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement Download Service/Tracking, Progress UI, Download Cancel UI …
…& Misc. UI Fixes (#360) * Added: System for Calculating Job Throughput * Added: Additional Rate Limiter States & Collective Throughput * Added: Progress Tracking for Downloads * WIP: DownloadService * Fixed: Bad Code Written during PR Feedback * Added: Tests for DownloadService and NXM+HTTP Implementation * Added Download Bar Style for Download List * Updated: LoginManager Dependencies * Changed: Fixed remaining code after Rebase from Main * Initial Progress on 'In Progress' UI * Moved: Downloaders Code to Separate Project * [WIP] Some Progress on InProgress UI * Added: Post Merge Fixes * Added: Functional Download Status Bar Column * Added: Pause/Resume Column Type * Added: Finalized LayoutGrid Buttons & Moved Grid Columns out for Tidyness * Completed: The Visual Design of InProgress DataGrid * Added: Auto Tint to Blue on InProgress UI Elements When Download Active * Changed: Remove Duplicate Code between Design and Runtime ViewModels * Wire up the Remainder of the InProgress Menu UI Updates * Added: Show Progress in Bar * Fixed: Wrong binding order. * Fixed: Incorrect Structural Border Colour (Compared to Branding on FIGMA) * Added: H6 Montserrat & Fixed Incorrect Text Colour on Body2RobotoRegular * Added: Ok/Cancel MessageBox * Added: Cancel Download Overlay * Fixed: Typo in CancelDownloadOverlayView * Fixed: Incorrectly swapped button hover and pressed styles. * temp commit please ignore [travelling soon, need to transfer to laptop] * Wire up Actual Data with InProgress UI * Wire up most of the remainder of the UI * Added: Polling for DownloadTaskViewModel * Fixed: Performance Issue with FoundGamesView Control * Removed: Dead TODO * Moved: Login Overlay & Added new Queue Based Overlay System * Changed: Use `BindToClasses` helper method. * Added: Tests for InProgressView & Accompanying Bug Fixes * Fixed: Non-Property SetOverlayItem caused Binding Failures * Fixed: Post Merge usings * Added: Tests for Ok/Cancel Modal * Added: Basic tests for DownloadTaskViewModel * Added: Missing Lifetime for Download Tasks * Added: Missing Thread Specifiers * Fixed: `DefaultModName` being Mistakenly Ignored after DataStore Fix * Added: Missing Selected Item Binding & Fix Cancel in Actual App * Fixed: Missing TaskCompletionSource to dismiss UI element. * Fixed: Deadlock in (Http/Nxm)DownloadTask. While waiting with Async is a possibility, I chose to remove the wait entirely, as to keep UI responsiveness high (these are called from UI). There is no side effect to this; as no resources (e.g. file handles) are shared with the outside. * Changed: StackPanel to Grid to Fix DataGrid Size Issue * Added: Existing In Progress Downloads upon initialisation of Downloads ViewModel * Remove Dead Object & Comment Deliberate Choice of Constraint * Changed: Refactor out common grid components and separate loadout/download grid by enum. * Use Humanizer in the display of byte sizes * Changed: Adjusted column widths to accommodate altered version column size. --------- Co-authored-by: Timothy Baldridge <tbaldridge@gmail.com>
- Loading branch information
Showing
164 changed files
with
4,226 additions
and
383 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
94 changes: 94 additions & 0 deletions
94
src/Networking/NexusMods.Networking.Downloaders/DownloadService.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
using System.Reactive.Subjects; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using NexusMods.DataModel.Loadouts; | ||
using NexusMods.DataModel.RateLimiting; | ||
using NexusMods.Networking.Downloaders.Interfaces; | ||
using NexusMods.Networking.NexusWebApi.Types; | ||
using NexusMods.Paths; | ||
|
||
namespace NexusMods.Networking.Downloaders; | ||
|
||
/// <inheritdoc /> | ||
public class DownloadService : IDownloadService | ||
{ | ||
/// <inheritdoc /> | ||
public List<IDownloadTask> Downloads { get; } = new(); | ||
|
||
private readonly IServiceProvider _provider; | ||
private readonly Subject<IDownloadTask> _started = new(); | ||
private readonly Subject<IDownloadTask> _completed = new(); | ||
private readonly Subject<IDownloadTask> _cancelled = new(); | ||
private readonly Subject<IDownloadTask> _paused = new(); | ||
private readonly Subject<IDownloadTask> _resumed = new(); | ||
|
||
public DownloadService(IServiceProvider provider) | ||
{ | ||
_provider = provider; | ||
} | ||
|
||
/// <inheritdoc /> | ||
public IObservable<IDownloadTask> StartedTasks => _started; | ||
|
||
/// <inheritdoc /> | ||
public IObservable<IDownloadTask> CompletedTasks => _completed; | ||
|
||
/// <inheritdoc /> | ||
public IObservable<IDownloadTask> CancelledTasks => _cancelled; | ||
|
||
/// <inheritdoc /> | ||
public IObservable<IDownloadTask> PausedTasks => _paused; | ||
|
||
/// <inheritdoc /> | ||
public IObservable<IDownloadTask> ResumedTasks => _resumed; | ||
|
||
/// <inheritdoc /> | ||
public void AddNxmTask(NXMUrl url) | ||
{ | ||
var task = _provider.GetRequiredService<NxmDownloadTask>(); | ||
task.Init(url); | ||
AddTask(task); | ||
} | ||
|
||
/// <inheritdoc /> | ||
public void AddHttpTask(string url, Loadout loadout) | ||
{ | ||
var task = _provider.GetRequiredService<HttpDownloadTask>(); | ||
task.Init(url, loadout); | ||
AddTask(task); | ||
} | ||
|
||
/// <inheritdoc /> | ||
public void AddTask(IDownloadTask task) | ||
{ | ||
Downloads.Add(task); | ||
_ = task.StartAsync(); | ||
_started.OnNext(task); | ||
} | ||
|
||
/// <inheritdoc /> | ||
public void OnComplete(IDownloadTask task) | ||
{ | ||
_completed.OnNext(task); | ||
} | ||
|
||
/// <inheritdoc /> | ||
public void OnCancelled(IDownloadTask task) | ||
{ | ||
_cancelled.OnNext(task); | ||
} | ||
|
||
/// <inheritdoc /> | ||
public void OnPaused(IDownloadTask task) | ||
{ | ||
_paused.OnNext(task); | ||
} | ||
|
||
/// <inheritdoc /> | ||
public void OnResumed(IDownloadTask task) | ||
{ | ||
_resumed.OnNext(task); | ||
} | ||
|
||
/// <inheritdoc /> | ||
public Size GetThroughput() => Downloads.SelectMany(x => x.DownloadJobs).GetTotalThroughput(new DateTimeProvider()); | ||
} |
138 changes: 138 additions & 0 deletions
138
src/Networking/NexusMods.Networking.Downloaders/HttpDownloadTask.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
using Microsoft.Extensions.Logging; | ||
using NexusMods.DataModel.Abstractions; | ||
using NexusMods.DataModel.Loadouts; | ||
using NexusMods.DataModel.RateLimiting; | ||
using NexusMods.Networking.Downloaders.Interfaces; | ||
using NexusMods.Networking.Downloaders.Interfaces.Traits; | ||
using NexusMods.Networking.HttpDownloader; | ||
using NexusMods.Paths; | ||
|
||
namespace NexusMods.Networking.Downloaders; | ||
|
||
/// <summary> | ||
/// Represents an individual task to download and install a .nxm link. | ||
/// </summary> | ||
/// <remarks> | ||
/// This task is usually created via <see cref="DownloadService.AddNxmTask"/>. | ||
/// </remarks> | ||
public class HttpDownloadTask : IDownloadTask, IHaveFileSize | ||
{ | ||
private string _url = null!; | ||
private readonly ILogger<HttpDownloadTask> _logger; | ||
private readonly TemporaryFileManager _temp; | ||
private readonly HttpClient _client; | ||
private readonly IHttpDownloader _downloader; | ||
private readonly IArchiveAnalyzer _archiveAnalyzer; | ||
private readonly IArchiveInstaller _archiveInstaller; | ||
private readonly CancellationTokenSource _tokenSource; | ||
private readonly HttpDownloaderState _state; | ||
private Loadout? _loadout; | ||
private Task? _task; | ||
|
||
/// <inheritdoc /> | ||
public IEnumerable<IJob<Size>> DownloadJobs => _state.Jobs; | ||
|
||
/// <inheritdoc /> | ||
public DownloadService Owner { get; } | ||
|
||
/// <inheritdoc /> | ||
public DownloadTaskStatus Status { get; private set; } = DownloadTaskStatus.Idle; | ||
|
||
/// <inheritdoc /> | ||
public string FriendlyName { get; private set; } = "Unknown HTTP Download"; | ||
|
||
/// <inheritdoc /> | ||
public long SizeBytes { get; private set; } = -1; | ||
|
||
/// <summary/> | ||
/// <remarks> | ||
/// This constructor is intended to be called from Dependency Injector. | ||
/// After running this constructor, you will need to run | ||
/// </remarks> | ||
public HttpDownloadTask(ILogger<HttpDownloadTask> logger, TemporaryFileManager temp, HttpClient client, IHttpDownloader downloader, IArchiveAnalyzer archiveAnalyzer, IArchiveInstaller archiveInstaller, DownloadService owner) | ||
{ | ||
_logger = logger; | ||
_temp = temp; | ||
_client = client; | ||
_downloader = downloader; | ||
_archiveAnalyzer = archiveAnalyzer; | ||
_archiveInstaller = archiveInstaller; | ||
_tokenSource = new CancellationTokenSource(); | ||
_state = new HttpDownloaderState(); | ||
Owner = owner; | ||
} | ||
|
||
/// <summary> | ||
/// Initializes components of this task that cannot be DI Injected. | ||
/// </summary> | ||
public void Init(string url, Loadout loadout) | ||
{ | ||
_url = url; | ||
_loadout = loadout; | ||
} | ||
|
||
public Task StartAsync() | ||
{ | ||
_task = StartImpl(); | ||
return _task; | ||
} | ||
|
||
private async Task StartImpl() | ||
{ | ||
var token = _tokenSource.Token; | ||
await using var tempPath = _temp.CreateFile(); | ||
|
||
Status = DownloadTaskStatus.Downloading; | ||
var nameSize = await GetNameAndSize(); | ||
FriendlyName = nameSize.FileName; | ||
SizeBytes = nameSize.FileSize; | ||
var request = new HttpRequestMessage(HttpMethod.Get, _url); | ||
await _downloader.DownloadAsync(new[] { request }, tempPath, _state, Size.FromLong(SizeBytes <= 0 ? 0 : SizeBytes), token); | ||
|
||
Status = DownloadTaskStatus.Installing; | ||
var analyzed = await _archiveAnalyzer.AnalyzeFileAsync(tempPath, token:token); | ||
await _archiveInstaller.AddMods(_loadout!.LoadoutId, analyzed.Hash, nameSize.FileName, token); | ||
|
||
Status = DownloadTaskStatus.Completed; | ||
Owner.OnComplete(this); | ||
} | ||
|
||
private async Task<GetNameAndSizeResult> GetNameAndSize() | ||
{ | ||
var uri = new Uri(_url); | ||
if (uri.IsFile) | ||
return new GetNameAndSizeResult(string.Empty, -1); | ||
|
||
var response = await _client.GetAsync(_url, HttpCompletionOption.ResponseHeadersRead); | ||
if (!response.IsSuccessStatusCode) | ||
{ | ||
_logger.LogWarning("HTTP request {Url} failed with status {ResponseStatusCode}", _url, response.StatusCode); | ||
return new GetNameAndSizeResult(string.Empty, -1); | ||
} | ||
|
||
// Get the filename from the Content-Disposition header, or default to a temporary file name. | ||
var contentDispositionHeader = response.Content.Headers.ContentDisposition?.FileNameStar | ||
?? response.Content.Headers.ContentDisposition?.FileName | ||
?? Path.GetTempFileName(); | ||
|
||
return new GetNameAndSizeResult(contentDispositionHeader.Trim('"'), response.Content.Headers.ContentLength.GetValueOrDefault(0)); | ||
} | ||
|
||
public void Cancel() | ||
{ | ||
try { _tokenSource.Cancel(); } | ||
catch (Exception) { /* ignored */ } | ||
|
||
// Do not _task.Wait() here, as it will deadlock without async. | ||
Owner.OnCancelled(this); | ||
} | ||
|
||
public void Pause() | ||
{ | ||
Status = DownloadTaskStatus.Paused; | ||
throw new NotImplementedException(); | ||
} | ||
|
||
public void Resume() => throw new NotImplementedException(); | ||
private record GetNameAndSizeResult(string FileName, long FileSize); | ||
} |
95 changes: 95 additions & 0 deletions
95
src/Networking/NexusMods.Networking.Downloaders/Interfaces/IDownloadService.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
using NexusMods.DataModel.Loadouts; | ||
using NexusMods.DataModel.RateLimiting; | ||
using NexusMods.Networking.NexusWebApi.Types; | ||
using NexusMods.Paths; | ||
|
||
namespace NexusMods.Networking.Downloaders.Interfaces; | ||
|
||
/// <summary> | ||
/// This is a 'download manager' of sorts. | ||
/// This service contains all of the downloads which have begun, or have already started. | ||
/// </summary> | ||
public interface IDownloadService | ||
{ | ||
/// <summary> | ||
/// Contains all downloads managed by the application. | ||
/// </summary> | ||
List<IDownloadTask> Downloads { get; } | ||
|
||
/// <summary> | ||
/// This gets fired whenever a download-and-install task is started. | ||
/// </summary> | ||
IObservable<IDownloadTask> StartedTasks { get; } | ||
|
||
/// <summary> | ||
/// This gets fired whenever a status of download-and-install task is completed. | ||
/// This happens when <see cref="JobState.Finished"/> is true. | ||
/// </summary> | ||
IObservable<IDownloadTask> CompletedTasks { get; } | ||
|
||
/// <summary> | ||
/// This gets fired whenever a status of download-and-install task is completed. | ||
/// This happens when <see cref="JobState.Finished"/> is true. | ||
/// </summary> | ||
IObservable<IDownloadTask> CancelledTasks { get; } | ||
|
||
/// <summary> | ||
/// This gets fired whenever a download-and-install task is paused. | ||
/// This happens when <see cref="JobState.Paused"/> is true. | ||
/// </summary> | ||
IObservable<IDownloadTask> PausedTasks { get; } | ||
|
||
/// <summary> | ||
/// This gets fired whenever a download-and-install task is resumed. | ||
/// This happens when <see cref="JobState.Running"/> is true after <see cref="JobState.Paused"/>. | ||
/// </summary> | ||
IObservable<IDownloadTask> ResumedTasks { get; } | ||
|
||
/// <summary> | ||
/// Adds a task that will download from a NXM link. | ||
/// </summary> | ||
/// <param name="url">Url to download from.</param> | ||
void AddNxmTask(NXMUrl url); | ||
|
||
/// <summary> | ||
/// Adds a task that will download from a HTTP link. | ||
/// </summary> | ||
/// <param name="url">Url to download from.</param> | ||
/// <param name="loadout">Loadout for the task.</param> | ||
void AddHttpTask(string url, Loadout loadout); | ||
|
||
/// <summary> | ||
/// Adds a task to the download queue. | ||
/// </summary> | ||
/// <param name="task">A task which has not yet been started.</param> | ||
void AddTask(IDownloadTask task); | ||
|
||
/// <summary> | ||
/// This is a callback fired by individual implementations of <see cref="IDownloadTask"/>. | ||
/// Fires off the necessary events. | ||
/// </summary> | ||
void OnComplete(IDownloadTask task); | ||
|
||
/// <summary> | ||
/// This is a callback fired by individual implementations of <see cref="IDownloadTask"/>. | ||
/// Fires off the necessary events. | ||
/// </summary> | ||
void OnCancelled(IDownloadTask task); | ||
|
||
/// <summary> | ||
/// This is a callback fired by individual implementations of <see cref="IDownloadTask"/>. | ||
/// Fires off the necessary events. | ||
/// </summary> | ||
void OnPaused(IDownloadTask task); | ||
|
||
/// <summary> | ||
/// This is a callback fired by individual implementations of <see cref="IDownloadTask"/>. | ||
/// Fires off the necessary events. | ||
/// </summary> | ||
void OnResumed(IDownloadTask task); | ||
|
||
/// <summary> | ||
/// Gets the total throughput of all download operations in bytes per second. | ||
/// </summary> | ||
Size GetThroughput(); | ||
} |
Oops, something went wrong.