Merge pull request #1942 from wabbajack-tools/tab-browsing-old

Tab browsing removed
This commit is contained in:
Timothy Baldridge
2022-05-20 16:34:59 -06:00
committed by GitHub
53 changed files with 1031 additions and 498 deletions

View File

@ -3,17 +3,20 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Wabbajack" xmlns:local="clr-namespace:Wabbajack"
xmlns:controls="clr-namespace:Wabbajack.Extensions"
ShutdownMode="OnExplicitShutdown" ShutdownMode="OnExplicitShutdown"
Startup="OnStartup" Startup="OnStartup"
Exit="OnExit"> Exit="OnExit">
<Application.Resources> <Application.Resources>
<ResourceDictionary> <ResourceDictionary>
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/Dark.Purple.xaml" /> <ResourceDictionary
Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/Dark.Purple.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" /> <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
<ResourceDictionary Source="Themes\Styles.xaml" /> <ResourceDictionary Source="Themes\Styles.xaml" />
<ResourceDictionary Source="Themes\CustomControls.xaml" /> <ResourceDictionary Source="Themes\CustomControls.xaml" />
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
</ResourceDictionary> </ResourceDictionary>
</Application.Resources> </Application.Resources>
</Application> </Application>

View File

@ -8,6 +8,8 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using ReactiveUI; using ReactiveUI;
using Wabbajack.DTOs; using Wabbajack.DTOs;
using Wabbajack.DTOs.Interventions;
using Wabbajack.Interventions;
using Wabbajack.LoginManagers; using Wabbajack.LoginManagers;
using Wabbajack.Models; using Wabbajack.Models;
using Wabbajack.Services.OSIntegrated; using Wabbajack.Services.OSIntegrated;
@ -46,9 +48,11 @@ namespace Wabbajack
services.AddOSIntegrated(); services.AddOSIntegrated();
services.AddSingleton<CefService>(); services.AddSingleton<CefService>();
services.AddSingleton<IUserInterventionHandler, UserInteventionHandler>();
services.AddTransient<MainWindow>(); services.AddTransient<MainWindow>();
services.AddTransient<MainWindowVM>(); services.AddTransient<MainWindowVM>();
services.AddTransient<BrowserWindow>();
services.AddSingleton<SystemParametersConstructor>(); services.AddSingleton<SystemParametersConstructor>();
services.AddSingleton<LauncherUpdater>(); services.AddSingleton<LauncherUpdater>();
services.AddSingleton<ResourceMonitor>(); services.AddSingleton<ResourceMonitor>();
@ -73,6 +77,8 @@ namespace Wabbajack
services.AddAllSingleton<INeedsLogin, LoversLabLoginManager>(); services.AddAllSingleton<INeedsLogin, LoversLabLoginManager>();
services.AddAllSingleton<INeedsLogin, NexusLoginManager>(); services.AddAllSingleton<INeedsLogin, NexusLoginManager>();
services.AddAllSingleton<INeedsLogin, VectorPlexusLoginManager>(); services.AddAllSingleton<INeedsLogin, VectorPlexusLoginManager>();
services.AddSingleton<ManualDownloadHandler>();
services.AddSingleton<ManualBlobDownloadHandler>();
return services; return services;
} }

View File

@ -0,0 +1,49 @@
using System;
using System.Reactive.Disposables;
using System.Windows.Threading;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ReactiveUI;
using Wabbajack.DTOs.Interventions;
using Wabbajack.Messages;
using Wabbajack.UserIntervention;
namespace Wabbajack.Interventions;
public class UserInteventionHandler : IUserInterventionHandler
{
private readonly ILogger<UserInteventionHandler> _logger;
private readonly IServiceProvider _serviceProvider;
public UserInteventionHandler(ILogger<UserInteventionHandler> logger, IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}
public void Raise(IUserIntervention intervention)
{
switch (intervention)
{
// Recast these or they won't be properly handled by the message bus
case ManualDownload md:
{
var provider = _serviceProvider.GetRequiredService<ManualDownloadHandler>();
provider.Intervention = md;
MessageBus.Current.SendMessage(new SpawnBrowserWindow(provider));
break;
}
case ManualBlobDownload bd:
{
var provider = _serviceProvider.GetRequiredService<ManualBlobDownloadHandler>();
provider.Intervention = bd;
MessageBus.Current.SendMessage(new SpawnBrowserWindow(provider));
break;
}
default:
_logger.LogError("No handler for user intervention: {Type}", intervention);
break;
}
}
}

View File

@ -7,6 +7,7 @@ using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using ReactiveUI; using ReactiveUI;
using ReactiveUI.Fody.Helpers; using ReactiveUI.Fody.Helpers;
@ -15,6 +16,7 @@ using Wabbajack.DTOs.Interventions;
using Wabbajack.DTOs.Logins; using Wabbajack.DTOs.Logins;
using Wabbajack.Messages; using Wabbajack.Messages;
using Wabbajack.Networking.Http.Interfaces; using Wabbajack.Networking.Http.Interfaces;
using Wabbajack.UserIntervention;
namespace Wabbajack.LoginManagers; namespace Wabbajack.LoginManagers;
@ -23,6 +25,7 @@ public class LoversLabLoginManager : ViewModel, INeedsLogin
private readonly ILogger<LoversLabLoginManager> _logger; private readonly ILogger<LoversLabLoginManager> _logger;
private readonly ITokenProvider<LoversLabLoginState> _token; private readonly ITokenProvider<LoversLabLoginState> _token;
private readonly IUserInterventionHandler _handler; private readonly IUserInterventionHandler _handler;
private readonly IServiceProvider _serviceProvider;
public string SiteName { get; } = "Lovers Lab"; public string SiteName { get; } = "Lovers Lab";
public ICommand TriggerLogin { get; set; } public ICommand TriggerLogin { get; set; }
@ -33,10 +36,11 @@ public class LoversLabLoginManager : ViewModel, INeedsLogin
[Reactive] [Reactive]
public bool HaveLogin { get; set; } public bool HaveLogin { get; set; }
public LoversLabLoginManager(ILogger<LoversLabLoginManager> logger, ITokenProvider<LoversLabLoginState> token) public LoversLabLoginManager(ILogger<LoversLabLoginManager> logger, ITokenProvider<LoversLabLoginState> token, IServiceProvider serviceProvider)
{ {
_logger = logger; _logger = logger;
_token = token; _token = token;
_serviceProvider = serviceProvider;
RefreshTokenState(); RefreshTokenState();
ClearLogin = ReactiveCommand.CreateFromTask(async () => ClearLogin = ReactiveCommand.CreateFromTask(async () =>
@ -52,11 +56,19 @@ public class LoversLabLoginManager : ViewModel, INeedsLogin
TriggerLogin = ReactiveCommand.CreateFromTask(async () => TriggerLogin = ReactiveCommand.CreateFromTask(async () =>
{ {
_logger.LogInformation("Logging into {SiteName}", SiteName); _logger.LogInformation("Logging into {SiteName}", SiteName);
await LoversLabLogin.Send(); StartLogin();
RefreshTokenState();
}, this.WhenAnyValue(v => v.HaveLogin).Select(v => !v)); }, this.WhenAnyValue(v => v.HaveLogin).Select(v => !v));
} }
private void StartLogin()
{
var view = new BrowserWindow();
view.Closed += (sender, args) => { RefreshTokenState(); };
var provider = _serviceProvider.GetRequiredService<LoversLabLoginHandler>();
view.DataContext = provider;
view.Show();
}
private void RefreshTokenState() private void RefreshTokenState()
{ {
HaveLogin = _token.HaveToken(); HaveLogin = _token.HaveToken();

View File

@ -52,12 +52,18 @@ public class NexusLoginManager : ViewModel, INeedsLogin
TriggerLogin = ReactiveCommand.CreateFromTask(async () => TriggerLogin = ReactiveCommand.CreateFromTask(async () =>
{ {
_logger.LogInformation("Logging into {SiteName}", SiteName); _logger.LogInformation("Logging into {SiteName}", SiteName);
MessageBus.Current.SendMessage(new OpenBrowserTab(_serviceProvider.GetRequiredService<NexusLoginHandler>())); //MessageBus.Current.SendMessage(new OpenBrowserTab(_serviceProvider.GetRequiredService<NexusLoginHandler>()));
StartLogin();
}, this.WhenAnyValue(v => v.HaveLogin).Select(v => !v)); }, this.WhenAnyValue(v => v.HaveLogin).Select(v => !v));
}
MessageBus.Current.Listen<CloseBrowserTab>() private void StartLogin()
.Subscribe(x => RefreshTokenState()) {
.DisposeWith(CompositeDisposable); var view = new BrowserWindow();
view.Closed += (sender, args) => { RefreshTokenState(); };
var provider = _serviceProvider.GetRequiredService<NexusLoginHandler>();
view.DataContext = provider;
view.Show();
} }
private void RefreshTokenState() private void RefreshTokenState()

View File

@ -7,6 +7,7 @@ using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using ReactiveUI; using ReactiveUI;
using ReactiveUI.Fody.Helpers; using ReactiveUI.Fody.Helpers;
@ -15,6 +16,7 @@ using Wabbajack.DTOs.Interventions;
using Wabbajack.DTOs.Logins; using Wabbajack.DTOs.Logins;
using Wabbajack.Messages; using Wabbajack.Messages;
using Wabbajack.Networking.Http.Interfaces; using Wabbajack.Networking.Http.Interfaces;
using Wabbajack.UserIntervention;
namespace Wabbajack.LoginManagers; namespace Wabbajack.LoginManagers;
@ -23,6 +25,7 @@ public class VectorPlexusLoginManager : ViewModel, INeedsLogin
private readonly ILogger<VectorPlexusLoginManager> _logger; private readonly ILogger<VectorPlexusLoginManager> _logger;
private readonly ITokenProvider<VectorPlexusLoginState> _token; private readonly ITokenProvider<VectorPlexusLoginState> _token;
private readonly IUserInterventionHandler _handler; private readonly IUserInterventionHandler _handler;
private readonly IServiceProvider _serviceProvider;
public string SiteName { get; } = "Vector Plexus"; public string SiteName { get; } = "Vector Plexus";
public ICommand TriggerLogin { get; set; } public ICommand TriggerLogin { get; set; }
@ -33,10 +36,11 @@ public class VectorPlexusLoginManager : ViewModel, INeedsLogin
[Reactive] [Reactive]
public bool HaveLogin { get; set; } public bool HaveLogin { get; set; }
public VectorPlexusLoginManager(ILogger<VectorPlexusLoginManager> logger, ITokenProvider<VectorPlexusLoginState> token) public VectorPlexusLoginManager(ILogger<VectorPlexusLoginManager> logger, ITokenProvider<VectorPlexusLoginState> token, IServiceProvider serviceProvider)
{ {
_logger = logger; _logger = logger;
_token = token; _token = token;
_serviceProvider = serviceProvider;
RefreshTokenState(); RefreshTokenState();
ClearLogin = ReactiveCommand.CreateFromTask(async () => ClearLogin = ReactiveCommand.CreateFromTask(async () =>
@ -52,11 +56,21 @@ public class VectorPlexusLoginManager : ViewModel, INeedsLogin
TriggerLogin = ReactiveCommand.CreateFromTask(async () => TriggerLogin = ReactiveCommand.CreateFromTask(async () =>
{ {
_logger.LogInformation("Logging into {SiteName}", SiteName); _logger.LogInformation("Logging into {SiteName}", SiteName);
await VectorPlexusLogin.Send(); StartLogin();
RefreshTokenState();
}, this.WhenAnyValue(v => v.HaveLogin).Select(v => !v)); }, this.WhenAnyValue(v => v.HaveLogin).Select(v => !v));
} }
private void StartLogin()
{
var view = new BrowserWindow();
view.Closed += (sender, args) => { RefreshTokenState(); };
var provider = _serviceProvider.GetRequiredService<VectorPlexusLoginManager>();
view.DataContext = provider;
view.Show();
}
private void RefreshTokenState() private void RefreshTokenState()
{ {
HaveLogin = _token.HaveToken(); HaveLogin = _token.HaveToken();

View File

@ -1,11 +0,0 @@
namespace Wabbajack.Messages;
public class CloseBrowserTab
{
public BrowserTabViewModel ViewModel { get; init; }
public CloseBrowserTab(BrowserTabViewModel viewModel)
{
ViewModel = viewModel;
}
}

View File

@ -1,15 +0,0 @@
using System.Threading.Tasks;
using ReactiveUI;
namespace Wabbajack.Messages;
public class LoversLabLogin : ALoginMessage
{
public static Task Send()
{
var msg = new LoversLabLogin();
MessageBus.Current.SendMessage(msg);
return msg.CompletionSource.Task;
}
}

View File

@ -1,22 +0,0 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using ReactiveUI;
using Wabbajack.DTOs.Interventions;
using Wabbajack.Networking.Http.Interfaces;
namespace Wabbajack.Messages;
public class NexusLogin : ALoginMessage
{
public NexusLogin()
{
}
public static Task Send()
{
var msg = new NexusLogin();
MessageBus.Current.SendMessage(msg);
return msg.CompletionSource.Task;
}
}

View File

@ -1,11 +0,0 @@
namespace Wabbajack.Messages;
public class OpenBrowserTab
{
public BrowserTabViewModel ViewModel { get; set; }
public OpenBrowserTab(BrowserTabViewModel viewModel)
{
ViewModel = viewModel;
}
}

View File

@ -0,0 +1,5 @@
namespace Wabbajack.Messages;
public record SpawnBrowserWindow (BrowserWindowViewModel Vm)
{
}

View File

@ -1,15 +0,0 @@
using System.Threading.Tasks;
using ReactiveUI;
namespace Wabbajack.Messages;
public class VectorPlexusLogin : ALoginMessage
{
public static Task Send()
{
var msg = new VectorPlexusLogin();
MessageBus.Current.SendMessage(msg);
return msg.CompletionSource.Task;
}
}

View File

@ -7,6 +7,7 @@ using System.Reactive.Subjects;
using System.Timers; using System.Timers;
using DynamicData; using DynamicData;
using DynamicData.Kernel; using DynamicData.Kernel;
using Microsoft.Extensions.Logging;
using ReactiveUI; using ReactiveUI;
using Wabbajack.RateLimiter; using Wabbajack.RateLimiter;
@ -14,7 +15,7 @@ namespace Wabbajack.Models;
public class ResourceMonitor : IDisposable public class ResourceMonitor : IDisposable
{ {
private readonly TimeSpan PollInterval = TimeSpan.FromMilliseconds(1000); private readonly TimeSpan PollInterval = TimeSpan.FromMilliseconds(250);
private readonly IResource[] _resources; private readonly IResource[] _resources;
private readonly Timer _timer; private readonly Timer _timer;
@ -27,13 +28,15 @@ public class ResourceMonitor : IDisposable
private readonly SourceCache<CPUDisplayVM, ulong> _tasks = new(x => x.ID); private readonly SourceCache<CPUDisplayVM, ulong> _tasks = new(x => x.ID);
public readonly ReadOnlyObservableCollection<CPUDisplayVM> _tasksFiltered; public readonly ReadOnlyObservableCollection<CPUDisplayVM> _tasksFiltered;
private readonly CompositeDisposable _compositeDisposable; private readonly CompositeDisposable _compositeDisposable;
private readonly ILogger<ResourceMonitor> _logger;
public ReadOnlyObservableCollection<CPUDisplayVM> Tasks => _tasksFiltered; public ReadOnlyObservableCollection<CPUDisplayVM> Tasks => _tasksFiltered;
public ResourceMonitor(IEnumerable<IResource> resources) public ResourceMonitor(ILogger<ResourceMonitor> logger, IEnumerable<IResource> resources)
{ {
_logger = logger;
_compositeDisposable = new CompositeDisposable(); _compositeDisposable = new CompositeDisposable();
_resources = resources.ToArray(); _resources = resources.ToArray();
_prev = _resources.Select(x => (x.Name, (long)0)).ToArray(); _prev = _resources.Select(x => (x.Name, (long)0)).ToArray();
@ -71,6 +74,7 @@ public class ResourceMonitor : IDisposable
var t = tsk.Value; var t = tsk.Value;
t.Msg = job.Description; t.Msg = job.Description;
t.ProgressPercent = job.Size == 0 ? Percent.Zero : Percent.FactoryPutInRange(job.Current, (long)job.Size); t.ProgressPercent = job.Size == 0 ? Percent.Zero : Percent.FactoryPutInRange(job.Current, (long)job.Size);
t.IsWorking = job.Current > 0;
} }
// Create // Create
@ -81,7 +85,8 @@ public class ResourceMonitor : IDisposable
ID = job.ID, ID = job.ID,
StartTime = DateTime.Now, StartTime = DateTime.Now,
Msg = job.Description, Msg = job.Description,
ProgressPercent = job.Size == 0 ? Percent.Zero : Percent.FactoryPutInRange(job.Current, (long) job.Size) ProgressPercent = job.Size == 0 ? Percent.Zero : Percent.FactoryPutInRange(job.Current, (long) job.Size),
IsWorking = job.Current > 0,
}; };
l.AddOrUpdate(vm); l.AddOrUpdate(vm);
} }

View File

@ -8,7 +8,7 @@ using Wabbajack.Services.OSIntegrated;
namespace Wabbajack.UserIntervention; namespace Wabbajack.UserIntervention;
public class LoversLabLoginHandler : OAuth2LoginHandler<Messages.LoversLabLogin, DTOs.Logins.LoversLabLoginState> public class LoversLabLoginHandler : OAuth2LoginHandler<DTOs.Logins.LoversLabLoginState>
{ {
public LoversLabLoginHandler(ILogger<LoversLabLoginHandler> logger, HttpClient httpClient, EncryptedJsonTokenProvider<LoversLabLoginState> tokenProvider) public LoversLabLoginHandler(ILogger<LoversLabLoginHandler> logger, HttpClient httpClient, EncryptedJsonTokenProvider<LoversLabLoginState> tokenProvider)
: base(logger, httpClient, tokenProvider) : base(logger, httpClient, tokenProvider)

View File

@ -0,0 +1,27 @@
using System.Threading;
using System.Threading.Tasks;
using Wabbajack.DTOs.DownloadStates;
using Wabbajack.DTOs.Interventions;
namespace Wabbajack.UserIntervention;
public class ManualBlobDownloadHandler : BrowserWindowViewModel
{
public ManualBlobDownload Intervention { get; set; }
protected override async Task Run(CancellationToken token)
{
await WaitForReady();
var archive = Intervention.Archive;
var md = Intervention.Archive.State as Manual;
HeaderText = $"Manual download ({md.Url.Host})";
Instructions = string.IsNullOrWhiteSpace(md.Prompt) ? $"Please download {archive.Name}" : md.Prompt;
var tsk = WaitForDownload(Intervention.Destination, token);
await NavigateTo(md.Url);
var hash = await tsk;
Intervention.Finish(hash);
}
}

View File

@ -0,0 +1,30 @@
using System.Security.Policy;
using System.Threading;
using System.Threading.Tasks;
using Wabbajack.DTOs;
using Wabbajack.DTOs.DownloadStates;
using Wabbajack.DTOs.Interventions;
using Wabbajack.Paths;
namespace Wabbajack.UserIntervention;
public class ManualDownloadHandler : BrowserWindowViewModel
{
public ManualDownload Intervention { get; set; }
protected override async Task Run(CancellationToken token)
{
//await WaitForReady();
var archive = Intervention.Archive;
var md = Intervention.Archive.State as Manual;
HeaderText = $"Manual download ({md.Url.Host})";
Instructions = string.IsNullOrWhiteSpace(md.Prompt) ? $"Please download {archive.Name}" : md.Prompt;
await NavigateTo(md.Url);
var uri = await WaitForDownloadUri(token);
Intervention.Finish(uri);
}
}

View File

@ -12,7 +12,7 @@ using Wabbajack.Services.OSIntegrated;
namespace Wabbajack.UserIntervention; namespace Wabbajack.UserIntervention;
public class NexusLoginHandler : BrowserTabViewModel public class NexusLoginHandler : BrowserWindowViewModel
{ {
private readonly EncryptedJsonTokenProvider<NexusApiState> _tokenProvider; private readonly EncryptedJsonTokenProvider<NexusApiState> _tokenProvider;

View File

@ -17,8 +17,7 @@ using Wabbajack.Services.OSIntegrated;
namespace Wabbajack.UserIntervention; namespace Wabbajack.UserIntervention;
public abstract class OAuth2LoginHandler<TIntervention, TLoginType> : WebUserInterventionBase<TIntervention> public abstract class OAuth2LoginHandler<TLoginType> : BrowserWindowViewModel
where TIntervention : IUserIntervention
where TLoginType : OAuth2LoginState, new() where TLoginType : OAuth2LoginState, new()
{ {
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;

View File

@ -7,7 +7,7 @@ using Wabbajack.Services.OSIntegrated;
namespace Wabbajack.UserIntervention; namespace Wabbajack.UserIntervention;
public class VectorPlexusLoginHandler : OAuth2LoginHandler<Messages.VectorPlexusLogin, DTOs.Logins.VectorPlexusLoginState> public class VectorPlexusLoginHandler : OAuth2LoginHandler<DTOs.Logins.VectorPlexusLoginState>
{ {
public VectorPlexusLoginHandler(ILogger<VectorPlexusLoginHandler> logger, HttpClient httpClient, EncryptedJsonTokenProvider<VectorPlexusLoginState> tokenProvider) public VectorPlexusLoginHandler(ILogger<VectorPlexusLoginHandler> logger, HttpClient httpClient, EncryptedJsonTokenProvider<VectorPlexusLoginState> tokenProvider)
: base(logger, httpClient, tokenProvider) : base(logger, httpClient, tokenProvider)

View File

@ -1,93 +0,0 @@
using System;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using HtmlAgilityPack;
using Microsoft.Web.WebView2.Core;
using Microsoft.Web.WebView2.Wpf;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.DTOs.Logins;
using Wabbajack.Messages;
using Wabbajack.Views;
namespace Wabbajack;
public abstract class BrowserTabViewModel : ViewModel
{
[Reactive] public string HeaderText { get; set; }
[Reactive] public string Instructions { get; set; }
public BrowserView? Browser { get; set; }
private WebView2 _browser => Browser!.Browser;
public async Task RunWrapper(CancellationToken token)
{
await Run(token);
MessageBus.Current.SendMessage(new CloseBrowserTab(this));
}
protected abstract Task Run(CancellationToken token);
protected async Task WaitForReady()
{
while (Browser?.Browser.CoreWebView2 == null)
{
await Task.Delay(250);
}
}
public async Task NavigateTo(Uri uri)
{
var tcs = new TaskCompletionSource();
void Completed(object? o, CoreWebView2NavigationCompletedEventArgs a)
{
if (a.IsSuccess)
{
tcs.TrySetResult();
}
else
{
tcs.TrySetException(new Exception($"Navigation error to {uri}"));
}
}
_browser.NavigationCompleted += Completed;
_browser.Source = uri;
await tcs.Task;
_browser.NavigationCompleted -= Completed;
}
public async Task<Cookie[]> GetCookies(string domainEnding, CancellationToken token)
{
var cookies = (await _browser.CoreWebView2.CookieManager.GetCookiesAsync(""))
.Where(c => c.Domain.EndsWith(domainEnding));
return cookies.Select(c => new Cookie
{
Domain = c.Domain,
Name = c.Name,
Path = c.Path,
Value = c.Value
}).ToArray();
}
public async Task<string> EvaluateJavaScript(string js)
{
return await _browser.ExecuteScriptAsync(js);
}
public async Task<HtmlDocument> GetDom(CancellationToken token)
{
var v = HttpUtility.UrlDecode("\u003D");
var source = await EvaluateJavaScript("document.body.outerHTML");
var decoded = JsonSerializer.Deserialize<string>(source);
var doc = new HtmlDocument();
doc.LoadHtml(decoded);
return doc;
}
}

View File

@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using HtmlAgilityPack;
using Microsoft.Web.WebView2.Core;
using Microsoft.Web.WebView2.Wpf;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.DTOs.Interventions;
using Wabbajack.DTOs.Logins;
using Wabbajack.Hashing.xxHash64;
using Wabbajack.Messages;
using Wabbajack.Paths;
using Wabbajack.Views;
namespace Wabbajack;
public abstract class BrowserWindowViewModel : ViewModel
{
[Reactive] public string HeaderText { get; set; }
[Reactive] public string Instructions { get; set; }
public BrowserWindow? Browser { get; set; }
private WebView2 _browser => Browser!.Browser;
public async Task RunWrapper(CancellationToken token)
{
await Run(token);
//MessageBus.Current.SendMessage(new CloseBrowserTab(this));
}
protected abstract Task Run(CancellationToken token);
protected async Task WaitForReady()
{
while (Browser?.Browser.CoreWebView2 == null)
{
await Task.Delay(250);
}
}
public async Task NavigateTo(Uri uri)
{
var tcs = new TaskCompletionSource();
void Completed(object? o, CoreWebView2NavigationCompletedEventArgs a)
{
if (a.IsSuccess)
{
tcs.TrySetResult();
}
else
{
if (a.WebErrorStatus == CoreWebView2WebErrorStatus.ConnectionAborted)
{
tcs.TrySetResult();
}
else
{
tcs.TrySetException(new Exception($"Navigation error to {uri} - {a.WebErrorStatus}"));
}
}
}
_browser.NavigationCompleted += Completed;
_browser.Source = uri;
await tcs.Task;
_browser.NavigationCompleted -= Completed;
}
public async Task<Cookie[]> GetCookies(string domainEnding, CancellationToken token)
{
var cookies = (await _browser.CoreWebView2.CookieManager.GetCookiesAsync(""))
.Where(c => c.Domain.EndsWith(domainEnding));
return cookies.Select(c => new Cookie
{
Domain = c.Domain,
Name = c.Name,
Path = c.Path,
Value = c.Value
}).ToArray();
}
public async Task<string> EvaluateJavaScript(string js)
{
return await _browser.ExecuteScriptAsync(js);
}
public async Task<HtmlDocument> GetDom(CancellationToken token)
{
var source = await EvaluateJavaScript("document.body.outerHTML");
var decoded = JsonSerializer.Deserialize<string>(source);
var doc = new HtmlDocument();
doc.LoadHtml(decoded);
return doc;
}
public async Task<ManualDownload.BrowserDownloadState> WaitForDownloadUri(CancellationToken token)
{
var source = new TaskCompletionSource<Uri>();
var referer = _browser.Source;
_browser.CoreWebView2.DownloadStarting += (sender, args) =>
{
try
{
source.SetResult(new Uri(args.DownloadOperation.Uri));
}
catch (Exception ex)
{
source.SetCanceled();
}
args.Cancel = true;
args.Handled = true;
};
var uri = await source.Task.WaitAsync(token);
var cookies = await GetCookies(uri.Host, token);
return new ManualDownload.BrowserDownloadState(uri, cookies, new[]
{
("Referer", referer.ToString())
});
}
public async Task<Hash> WaitForDownload(AbsolutePath path, CancellationToken token)
{
var source = new TaskCompletionSource();
var referer = _browser.Source;
_browser.CoreWebView2.DownloadStarting += (sender, args) =>
{
try
{
args.ResultFilePath = path.ToString();
args.Handled = true;
args.DownloadOperation.StateChanged += (o, o1) =>
{
var operation = (CoreWebView2DownloadOperation) o;
if (operation.State == CoreWebView2DownloadState.Completed)
source.TrySetResult();
};
}
catch (Exception ex)
{
source.SetCanceled();
}
};
await source.Task;
return default;
}
}

View File

@ -122,25 +122,7 @@ namespace Wabbajack
ModListContentsCommend = ReactiveCommand.Create(async () => ModListContentsCommend = ReactiveCommand.Create(async () =>
{ {
_parent.MWVM.ModListContentsVM.Value.Name = metadata.Title; UIUtils.OpenWebsite(new Uri("https://www.wabbajack.org/search/" + Metadata.NamespacedName));
IsLoadingIdle.OnNext(false);
try
{
var status = await wjClient.GetDetailedStatus(metadata.NamespacedName);
var coll = _parent.MWVM.ModListContentsVM.Value.Status;
coll.Clear();
coll.AddRange(status.Archives.Select(a => new DetailedStatusItem
{
Archive = a.Original,
ArchiveStatus = a.Status,
IsFailing = a.Status != ArchiveStatus.InValid
}));
NavigateToGlobal.Send(NavigateToGlobal.ScreenType.ModListContents);
}
finally
{
IsLoadingIdle.OnNext(true);
}
}, IsLoadingIdle.StartWith(true)); }, IsLoadingIdle.StartWith(true));
ExecuteCommand = ReactiveCommand.CreateFromTask(async () => ExecuteCommand = ReactiveCommand.CreateFromTask(async () =>

View File

@ -6,6 +6,8 @@ using System.Windows.Media.Imaging;
using ReactiveUI.Fody.Helpers; using ReactiveUI.Fody.Helpers;
using DynamicData; using DynamicData;
using System.Reactive; using System.Reactive;
using System.Reactive.Linq;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Shell; using System.Windows.Shell;
@ -112,6 +114,7 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM
// Command properties // Command properties
public ReactiveCommand<Unit, Unit> ShowManifestCommand { get; } public ReactiveCommand<Unit, Unit> ShowManifestCommand { get; }
public ReactiveCommand<Unit, Unit> OpenReadmeCommand { get; } public ReactiveCommand<Unit, Unit> OpenReadmeCommand { get; }
public ReactiveCommand<Unit, Unit> OpenDiscordButton { get; }
public ReactiveCommand<Unit, Unit> VisitModListWebsiteCommand { get; } public ReactiveCommand<Unit, Unit> VisitModListWebsiteCommand { get; }
public ReactiveCommand<Unit, Unit> CloseWhenCompleteCommand { get; } public ReactiveCommand<Unit, Unit> CloseWhenCompleteCommand { get; }
@ -164,6 +167,25 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM
UIUtils.OpenFolder(_configuration.LogLocation); UIUtils.OpenFolder(_configuration.LogLocation);
}); });
OpenDiscordButton = ReactiveCommand.Create(() =>
{
UIUtils.OpenWebsite(new Uri(ModlistMetadata.Links.DiscordURL));
}, this.WhenAnyValue(x => x.ModlistMetadata)
.WhereNotNull()
.Select(md => !string.IsNullOrWhiteSpace(md.Links.DiscordURL)));
ShowManifestCommand = ReactiveCommand.Create(() =>
{
UIUtils.OpenWebsite(new Uri("https://www.wabbajack.org/search/" + ModlistMetadata.NamespacedName));
}, this.WhenAnyValue(x => x.ModlistMetadata)
.WhereNotNull()
.Select(md => !string.IsNullOrWhiteSpace(md.Links.MachineURL)));
CloseWhenCompleteCommand = ReactiveCommand.Create(() =>
{
Environment.Exit(0);
});
GoToInstallCommand = ReactiveCommand.Create(() => GoToInstallCommand = ReactiveCommand.Create(() =>
{ {
UIUtils.OpenFolder(Installer.Location.TargetPath); UIUtils.OpenFolder(Installer.Location.TargetPath);
@ -214,6 +236,20 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM
var hex = (await ModListLocation.TargetPath.ToString().Hash()).ToHex(); var hex = (await ModListLocation.TargetPath.ToString().Hash()).ToHex();
var prevSettings = await _settingsManager.Load<SavedInstallSettings>(InstallSettingsPrefix + hex); var prevSettings = await _settingsManager.Load<SavedInstallSettings>(InstallSettingsPrefix + hex);
if (path.WithExtension(Ext.MetaData).FileExists())
{
try
{
metadata = JsonSerializer.Deserialize<ModlistMetadata>(await path.WithExtension(Ext.MetaData)
.ReadAllTextAsync());
ModlistMetadata = metadata;
}
catch (Exception ex)
{
_logger.LogInformation(ex, "Can't load metadata cached next to file");
}
}
if (prevSettings.ModListLocation == path) if (prevSettings.ModListLocation == path)
{ {
ModListLocation.TargetPath = prevSettings.ModListLocation; ModListLocation.TargetPath = prevSettings.ModListLocation;
@ -236,48 +272,61 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM
private async Task BeginInstall() private async Task BeginInstall()
{ {
InstallState = InstallState.Installing; await Task.Run(async () =>
var postfix = (await ModListLocation.TargetPath.ToString().Hash()).ToHex();
await _settingsManager.Save(InstallSettingsPrefix + postfix, new SavedInstallSettings
{ {
ModListLocation = ModListLocation.TargetPath, InstallState = InstallState.Installing;
InstallLocation = Installer.Location.TargetPath, var postfix = (await ModListLocation.TargetPath.ToString().Hash()).ToHex();
DownloadLoadction = Installer.DownloadLocation.TargetPath, await _settingsManager.Save(InstallSettingsPrefix + postfix, new SavedInstallSettings
Metadata = ModlistMetadata
});
try
{
var installer = StandardInstaller.Create(_serviceProvider, new InstallerConfiguration
{ {
Game = ModList.GameType, ModListLocation = ModListLocation.TargetPath,
Downloads = Installer.DownloadLocation.TargetPath, InstallLocation = Installer.Location.TargetPath,
Install = Installer.Location.TargetPath, DownloadLoadction = Installer.DownloadLocation.TargetPath,
ModList = ModList, Metadata = ModlistMetadata
ModlistArchive = ModListLocation.TargetPath,
SystemParameters = _parametersConstructor.Create(),
GameFolder = _gameLocator.GameLocation(ModList.GameType)
}); });
try
installer.OnStatusUpdate = update =>
{ {
StatusText = update.StatusText; var installer = StandardInstaller.Create(_serviceProvider, new InstallerConfiguration
StatusProgress = update.StepsProgress; {
Game = ModList.GameType,
Downloads = Installer.DownloadLocation.TargetPath,
Install = Installer.Location.TargetPath,
ModList = ModList,
ModlistArchive = ModListLocation.TargetPath,
SystemParameters = _parametersConstructor.Create(),
GameFolder = _gameLocator.GameLocation(ModList.GameType)
});
TaskBarUpdate.Send(update.StatusText, TaskbarItemProgressState.Indeterminate, update.StepsProgress.Value);
};
await installer.Begin(CancellationToken.None);
TaskBarUpdate.Send($"Finished install of {ModList.Name}", TaskbarItemProgressState.Normal); installer.OnStatusUpdate = update =>
{
StatusText = update.StatusText;
StatusProgress = update.StepsProgress;
InstallState = InstallState.Success; TaskBarUpdate.Send(update.StatusText, TaskbarItemProgressState.Indeterminate,
} update.StepsProgress.Value);
catch (Exception ex) };
{ if (!await installer.Begin(CancellationToken.None))
TaskBarUpdate.Send($"Error during install of {ModList.Name}", TaskbarItemProgressState.Error); {
InstallState = InstallState.Failure; TaskBarUpdate.Send($"Error during install of {ModList.Name}", TaskbarItemProgressState.Error);
} InstallState = InstallState.Failure;
StatusText = $"Error during install of {ModList.Name}";
StatusProgress = Percent.Zero;
}
else
{
TaskBarUpdate.Send($"Finished install of {ModList.Name}", TaskbarItemProgressState.Normal);
InstallState = InstallState.Success;
}
}
catch (Exception ex)
{
TaskBarUpdate.Send($"Error during install of {ModList.Name}", TaskbarItemProgressState.Error);
InstallState = InstallState.Failure;
StatusText = $"Error during install of {ModList.Name}";
StatusProgress = Percent.Zero;
}
});
} }

View File

@ -15,6 +15,7 @@ using Wabbajack;
using Wabbajack.DTOs; using Wabbajack.DTOs;
using Wabbajack.DTOs.Interventions; using Wabbajack.DTOs.Interventions;
using Wabbajack.Interventions; using Wabbajack.Interventions;
using Wabbajack.Paths;
using Wabbajack.Util; using Wabbajack.Util;
namespace Wabbajack namespace Wabbajack
@ -53,6 +54,15 @@ namespace Wabbajack
PathType = FilePickerVM.PathTypeOptions.Folder, PathType = FilePickerVM.PathTypeOptions.Folder,
PromptTitle = "Select Installation Directory", PromptTitle = "Select Installation Directory",
}; };
Location.WhenAnyValue(t => t.TargetPath)
.Subscribe(newPath =>
{
if (newPath != default && DownloadLocation!.TargetPath == AbsolutePath.Empty)
{
DownloadLocation.TargetPath = newPath.Combine("downloads");
}
}).DisposeWith(CompositeDisposable);
DownloadLocation = new FilePickerVM() DownloadLocation = new FilePickerVM()
{ {
ExistCheckOption = FilePickerVM.CheckOptions.Off, ExistCheckOption = FilePickerVM.CheckOptions.Off,

View File

@ -17,6 +17,7 @@ using Microsoft.Extensions.Logging;
using Wabbajack.Common; using Wabbajack.Common;
using Wabbajack.Downloaders.GameFile; using Wabbajack.Downloaders.GameFile;
using Wabbajack; using Wabbajack;
using Wabbajack.DTOs.Interventions;
using Wabbajack.Interventions; using Wabbajack.Interventions;
using Wabbajack.LoginManagers; using Wabbajack.LoginManagers;
using Wabbajack.Messages; using Wabbajack.Messages;
@ -67,6 +68,9 @@ namespace Wabbajack
[Reactive] [Reactive]
public string ResourceStatus { get; set; } public string ResourceStatus { get; set; }
[Reactive]
public string AppName { get; set; }
[Reactive] [Reactive]
public bool UpdateAvailable { get; private set; } public bool UpdateAvailable { get; private set; }
@ -101,16 +105,9 @@ namespace Wabbajack
.Subscribe(HandleNavigateBack) .Subscribe(HandleNavigateBack)
.DisposeWith(CompositeDisposable); .DisposeWith(CompositeDisposable);
MessageBus.Current.Listen<NexusLogin>() MessageBus.Current.Listen<SpawnBrowserWindow>()
.Subscribe(HandleLogin) .ObserveOnGuiThread()
.DisposeWith(CompositeDisposable); .Subscribe(HandleSpawnBrowserWindow)
MessageBus.Current.Listen<LoversLabLogin>()
.Subscribe(HandleLogin)
.DisposeWith(CompositeDisposable);
MessageBus.Current.Listen<VectorPlexusLogin>()
.Subscribe(HandleLogin)
.DisposeWith(CompositeDisposable); .DisposeWith(CompositeDisposable);
_resourceMonitor.Updates _resourceMonitor.Updates
@ -136,6 +133,7 @@ namespace Wabbajack
var fvi = FileVersionInfo.GetVersionInfo(assembly.Location); var fvi = FileVersionInfo.GetVersionInfo(assembly.Location);
Consts.CurrentMinimumWabbajackVersion = Version.Parse(fvi.FileVersion); Consts.CurrentMinimumWabbajackVersion = Version.Parse(fvi.FileVersion);
VersionDisplay = $"v{fvi.FileVersion}"; VersionDisplay = $"v{fvi.FileVersion}";
AppName = "WABBAJACK " + VersionDisplay;
_logger.LogInformation("Wabbajack Version: {FileVersion}", fvi.FileVersion); _logger.LogInformation("Wabbajack Version: {FileVersion}", fvi.FileVersion);
Task.Run(() => _wjClient.SendMetric("started_wabbajack", fvi.FileVersion)).FireAndForget(); Task.Run(() => _wjClient.SendMetric("started_wabbajack", fvi.FileVersion)).FireAndForget();
@ -162,31 +160,33 @@ namespace Wabbajack
ActivePane = objViewModel; ActivePane = objViewModel;
} }
private void HandleLogin(NexusLogin nexusLogin)
{
var handler = _serviceProvider.GetRequiredService<NexusLoginHandler>();
handler.RunWrapper(CancellationToken.None).FireAndForget();
}
private void HandleLogin(LoversLabLogin loversLabLogin)
{
var handler = _serviceProvider.GetRequiredService<LoversLabLoginHandler>();
handler.RunWrapper(CancellationToken.None).FireAndForget();
}
private void HandleLogin(VectorPlexusLogin vectorPlexusLogin)
{
var handler = _serviceProvider.GetRequiredService<VectorPlexusLoginHandler>();
handler.RunWrapper(CancellationToken.None).FireAndForget();
}
private void HandleNavigateBack(NavigateBack navigateBack) private void HandleNavigateBack(NavigateBack navigateBack)
{ {
ActivePane = PreviousPanes.Last(); ActivePane = PreviousPanes.Last();
PreviousPanes.RemoveAt(PreviousPanes.Count - 1); PreviousPanes.RemoveAt(PreviousPanes.Count - 1);
} }
private void HandleManualDownload(ManualDownload manualDownload)
{
var handler = _serviceProvider.GetRequiredService<ManualDownloadHandler>();
handler.Intervention = manualDownload;
//MessageBus.Current.SendMessage(new OpenBrowserTab(handler));
}
private void HandleManualBlobDownload(ManualBlobDownload manualDownload)
{
var handler = _serviceProvider.GetRequiredService<ManualBlobDownloadHandler>();
handler.Intervention = manualDownload;
//MessageBus.Current.SendMessage(new OpenBrowserTab(handler));
}
private void HandleSpawnBrowserWindow(SpawnBrowserWindow msg)
{
var window = _serviceProvider.GetRequiredService<BrowserWindow>();
window.DataContext = msg.Vm;
window.Show();
}
private void HandleNavigateTo(NavigateToGlobal.ScreenType s) private void HandleNavigateTo(NavigateToGlobal.ScreenType s)
{ {
if (s is NavigateToGlobal.ScreenType.Settings) if (s is NavigateToGlobal.ScreenType.Settings)

View File

@ -1,23 +0,0 @@
<TabItem x:Class="Wabbajack.Views.BrowserTabView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
xmlns:views="clr-namespace:Wabbajack.Views"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<TabItem.Style>
<Style TargetType="TabItem" BasedOn="{StaticResource {x:Type TabItem}}"></Style>
</TabItem.Style>
<TabItem.Header>
<StackPanel Orientation="Horizontal">
<TextBlock FontSize="16" x:Name="HeaderText">_</TextBlock>
</StackPanel>
</TabItem.Header>
<Grid>
<views:BrowserView x:Name="Browser">
</views:BrowserView>
</Grid>
</TabItem>

View File

@ -1,52 +0,0 @@
using System;
using System.Reactive.Disposables;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using ReactiveUI;
using Wabbajack.Common;
namespace Wabbajack.Views;
public partial class BrowserTabView : IDisposable
{
private readonly CompositeDisposable _compositeDisposable;
public BrowserTabView(BrowserTabViewModel vm)
{
_compositeDisposable = new CompositeDisposable();
InitializeComponent();
Browser.Browser.Source = new Uri("http://www.google.com");
vm.Browser = Browser;
DataContext = vm;
vm.WhenAnyValue(vm => vm.HeaderText)
.BindTo(this, view => view.HeaderText.Text)
.DisposeWith(_compositeDisposable);
Start().FireAndForget();
}
private async Task Start()
{
await ((BrowserTabViewModel) DataContext).RunWrapper(CancellationToken.None);
ClickClose(this, new RoutedEventArgs());
}
public void Dispose()
{
_compositeDisposable.Dispose();
var vm = (BrowserTabViewModel) DataContext;
vm.Browser = null;
}
private void ClickClose(object sender, RoutedEventArgs e)
{
var tc = (TabControl) this.Parent;
if (tc.Items.Contains(this))
tc.Items.Remove(this);
this.Dispose();
}
}

View File

@ -1,4 +1,4 @@
<reactiveUi:ReactiveUserControl x:TypeArguments="wabbajack:BrowserTabViewModel" <reactiveUi:ReactiveUserControl x:TypeArguments="wabbajack:BrowserWindowViewModel"
x:Class="Wabbajack.Views.BrowserView" x:Class="Wabbajack.Views.BrowserView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

View File

@ -0,0 +1,49 @@
<mahapps:MetroWindow x:Class="Wabbajack.BrowserWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:icon="http://metro.mahapps.com/winfx/xaml/iconpacks"
xmlns:local="clr-namespace:Wabbajack"
xmlns:mahapps="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:Wabbajack.View_Models"
xmlns:wpf="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
ShowTitleBar="False"
Title="Browser Window"
Width="1280"
Height="960"
MinWidth="850"
MinHeight="650"
RenderOptions.BitmapScalingMode="HighQuality"
ResizeMode="CanResize"
Style="{StaticResource {x:Type Window}}"
TitleBarHeight="25"
UseLayoutRounding="True"
WindowTitleBrush="{StaticResource MahApps.Brushes.Accent}"
ContentRendered="BrowserWindow_OnActivated"
mc:Ignorable="d">
<Grid Background="#121212" MouseDown="UIElement_OnMouseDown">
<Grid.RowDefinitions>
<RowDefinition Height="20"></RowDefinition>
<RowDefinition Height="20"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20"></ColumnDefinition>
<ColumnDefinition Width="20"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.ColumnSpan="3" FontSize="16">Browser Window</TextBlock>
<Button Grid.Row="1" Grid.Column="0">
<icon:PackIconModern Kind="NavigatePrevious"></icon:PackIconModern>
</Button>
<Button Grid.Row="1" Grid.Column="1">
<icon:PackIconModern Kind="Home"></icon:PackIconModern>
</Button>
<TextBox Grid.Row="1" Grid.Column="3" VerticalContentAlignment="Center"></TextBox>
<wpf:WebView2 Grid.Row="2" Grid.ColumnSpan="3" Name="Browser"></wpf:WebView2>
</Grid>
</mahapps:MetroWindow>

View File

@ -0,0 +1,29 @@
using System;
using System.Threading;
using System.Windows;
using System.Windows.Input;
using MahApps.Metro.Controls;
using Wabbajack.Common;
namespace Wabbajack;
public partial class BrowserWindow : MetroWindow
{
public BrowserWindow()
{
InitializeComponent();
}
private void UIElement_OnMouseDown(object sender, MouseButtonEventArgs e)
{
base.DragMove();
}
private void BrowserWindow_OnActivated(object sender, EventArgs e)
{
var vm = ((BrowserWindowViewModel) DataContext);
vm.Browser = this;
vm.RunWrapper(CancellationToken.None)
.ContinueWith(_ => Dispatcher.Invoke(Close));
}
}

View File

@ -24,7 +24,7 @@
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="4" <TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="5"
x:Name="TitleText" x:Name="TitleText"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Bottom" VerticalAlignment="Bottom"

View File

@ -212,37 +212,60 @@
<ColumnDefinition Width="4" /> <ColumnDefinition Width="4" />
<ColumnDefinition Width="2*" /> <ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Grid Grid.Column="0" Margin="10"> <Grid Grid.Column="0" Margin="0">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="*" /> <RowDefinition Height="*" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Button Grid.Row="0" <Button Grid.Row="0"
x:Name="OpenReadmePreInstallButton" x:Name="OpenDiscordPreInstallButton"
Margin="30,5" Margin="30,2"
FontSize="20" FontSize="20"
Style="{StaticResource LargeButtonStyle}" Style="{StaticResource LargeButtonStyle}"
ToolTip="Open the readme for the modlist"> ToolTip="Open the Discord for this Modlist">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="30" /> <ColumnDefinition Width="30" />
<ColumnDefinition Width="82" /> <ColumnDefinition Width="82" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<icon:PackIconFontAwesome Grid.Column="0" <icon:PackIconFontAwesome Grid.Column="0"
Width="30" Width="30"
Height="30" Height="30"
VerticalAlignment="Center" VerticalAlignment="Center"
Kind="ReadmeBrands" /> Kind="DiscordBrands" />
<TextBlock Grid.Column="1" <TextBlock Grid.Column="1"
Margin="10,0,0,0" Margin="10,0,0,0"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="Readme" /> Text="Discord" />
</Grid> </Grid>
</Button> </Button>
<Button Grid.Row="1" <Button Grid.Row="1"
x:Name="OpenReadmePreInstallButton"
Margin="30,2"
FontSize="20"
Style="{StaticResource LargeButtonStyle}"
ToolTip="Open the readme for the modlist">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30" />
<ColumnDefinition Width="82" />
</Grid.ColumnDefinitions>
<icon:PackIconFontAwesome Grid.Column="0"
Width="30"
Height="30"
VerticalAlignment="Center"
Kind="ReadmeBrands" />
<TextBlock Grid.Column="1"
Margin="10,0,0,0"
VerticalAlignment="Center"
Text="Readme" />
</Grid>
</Button>
<Button Grid.Row="2"
x:Name="VisitWebsitePreInstallButton" x:Name="VisitWebsitePreInstallButton"
Margin="30,5" Margin="30,2"
FontSize="20" FontSize="20"
Style="{StaticResource LargeButtonStyle}" Style="{StaticResource LargeButtonStyle}"
ToolTip="Open the webpage for the modlist"> ToolTip="Open the webpage for the modlist">
@ -262,9 +285,9 @@
Text="Website" /> Text="Website" />
</Grid> </Grid>
</Button> </Button>
<Button Grid.Row="2" <Button Grid.Row="3"
x:Name="ShowManifestPreInstallButton" x:Name="ShowManifestPreInstallButton"
Margin="30,5" Margin="30,2"
FontSize="20" FontSize="20"
Style="{StaticResource LargeButtonStyle}" Style="{StaticResource LargeButtonStyle}"
ToolTip="Open an explicit listing of all actions this modlist will take"> ToolTip="Open an explicit listing of all actions this modlist will take">

View File

@ -31,7 +31,7 @@ namespace Wabbajack
.DisposeWith(disposables); .DisposeWith(disposables);
ViewModel.WhenAnyValue(vm => vm.InstallState) ViewModel.WhenAnyValue(vm => vm.InstallState)
.Select(es => es == InstallState.Success ? Visibility.Visible : Visibility.Collapsed) .Select(es => es is InstallState.Success or InstallState.Failure ? Visibility.Visible : Visibility.Collapsed)
.BindToStrict(this, view => view.InstallComplete.Visibility) .BindToStrict(this, view => view.InstallComplete.Visibility)
.DisposeWith(disposables); .DisposeWith(disposables);
@ -43,10 +43,22 @@ namespace Wabbajack
.BindToStrict(this, view => view.OpenReadmePreInstallButton.Command) .BindToStrict(this, view => view.OpenReadmePreInstallButton.Command)
.DisposeWith(disposables); .DisposeWith(disposables);
ViewModel.WhenAnyValue(vm => vm.OpenDiscordButton)
.BindToStrict(this, view => view.OpenDiscordPreInstallButton.Command)
.DisposeWith(disposables);
ViewModel.WhenAnyValue(vm => vm.VisitModListWebsiteCommand) ViewModel.WhenAnyValue(vm => vm.VisitModListWebsiteCommand)
.BindToStrict(this, view => view.OpenWebsite.Command) .BindToStrict(this, view => view.OpenWebsite.Command)
.DisposeWith(disposables); .DisposeWith(disposables);
ViewModel.WhenAnyValue(vm => vm.VisitModListWebsiteCommand)
.BindToStrict(this, view => view.VisitWebsitePreInstallButton.Command)
.DisposeWith(disposables);
ViewModel.WhenAnyValue(vm => vm.ShowManifestCommand)
.BindToStrict(this, view => view.ShowManifestPreInstallButton.Command)
.DisposeWith(disposables);
ViewModel.WhenAnyValue(vm => vm.LoadingLock.IsLoading) ViewModel.WhenAnyValue(vm => vm.LoadingLock.IsLoading)
.Select(loading => loading ? Visibility.Visible : Visibility.Collapsed) .Select(loading => loading ? Visibility.Visible : Visibility.Collapsed)
.BindToStrict(this, view => view.ModlistLoadingRing.Visibility) .BindToStrict(this, view => view.ModlistLoadingRing.Visibility)

View File

@ -8,6 +8,7 @@
xmlns:mahapps="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro" xmlns:mahapps="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:Wabbajack.View_Models" xmlns:viewModels="clr-namespace:Wabbajack.View_Models"
xmlns:views="clr-namespace:Wabbajack.Views"
ShowTitleBar="False" ShowTitleBar="False"
Title="WABBAJACK" Title="WABBAJACK"
Width="1280" Width="1280"
@ -22,33 +23,28 @@
UseLayoutRounding="True" UseLayoutRounding="True"
WindowTitleBrush="{StaticResource MahApps.Brushes.Accent}" WindowTitleBrush="{StaticResource MahApps.Brushes.Accent}"
mc:Ignorable="d"> mc:Ignorable="d">
<Grid Background="#121212" MouseDown="UIElement_OnMouseDown">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<Rectangle Grid.Row="0">
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="#16BB86FC" />
<GradientStop Offset="0.4" Color="#00000000" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<TabControl Grid.Row="0" x:Name="Tabs">
<TabItem>
<TabItem.Header>
<StackPanel Orientation="Horizontal">
<TextBlock FontSize="16" Margin="0, 0, 8, 0">WABBAJACK 3.0.0</TextBlock>
<Button Name="SettingsButton"><icon:Material Kind="Cog"></icon:Material></Button>
</StackPanel>
</TabItem.Header>
<Grid>
<Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition> <RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<ContentPresenter Grid.Row="0" Content="{Binding ActivePane}"> <Grid Grid.Row="0" Margin="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="140"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" FontSize="16" Margin="0, 0, 8, 0" Name="AppName"></TextBlock>
<TextBlock Grid.Column="1" Margin="5, 0" Name="ResourceUsage" HorizontalAlignment="Right"></TextBlock>
<Button Grid.Column="2" Name="SettingsButton">
<icon:Material Kind="Cog"></icon:Material>
</Button>
</Grid>
<ContentPresenter Grid.Row="1" Content="{Binding ActivePane}">
<ContentPresenter.Resources> <ContentPresenter.Resources>
<DataTemplate DataType="{x:Type local:CompilerVM}"> <DataTemplate DataType="{x:Type local:CompilerVM}">
<local:CompilerView ViewModel="{Binding}" /> <local:CompilerView ViewModel="{Binding}" />
@ -73,11 +69,8 @@
</DataTemplate> </DataTemplate>
</ContentPresenter.Resources> </ContentPresenter.Resources>
</ContentPresenter> </ContentPresenter>
<TextBlock Grid.Row="1" Margin="5, 0" Name="ResourceUsage" HorizontalAlignment="Right"></TextBlock>
</Grid> </Grid>
</TabItem>
</TabControl>
</Grid>
<mahapps:MetroWindow.RightWindowCommands> <mahapps:MetroWindow.RightWindowCommands>
<mahapps:WindowCommands> <mahapps:WindowCommands>
@ -90,12 +83,12 @@
Command="{Binding CopyVersionCommand}" Command="{Binding CopyVersionCommand}"
Content="{Binding VersionDisplay}"> Content="{Binding VersionDisplay}">
<Button.ToolTip> <Button.ToolTip>
<ToolTip Content="Wabbajack Version&#x0a;Click to copy to clipboard"/> <ToolTip Content="Wabbajack Version&#x0a;Click to copy to clipboard" />
</Button.ToolTip> </Button.ToolTip>
</Button> </Button>
<Button Grid.Column="1" <Button Grid.Column="1"
Margin="5,0" Margin="5,0"
Command="{Binding OpenSettingsCommand}"> Command="{Binding OpenSettingsCommand}">
<icon:PackIconMaterial <icon:PackIconMaterial
Width="17" Width="17"
Height="17" Height="17"

View File

@ -1,16 +1,24 @@
using System; using System;
using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using DynamicData;
using DynamicData.Binding;
using MahApps.Metro.Controls; using MahApps.Metro.Controls;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using ReactiveUI; using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.Common; using Wabbajack.Common;
using Wabbajack.DTOs;
using Wabbajack.DTOs.DownloadStates;
using Wabbajack.DTOs.Interventions;
using Wabbajack.Messages; using Wabbajack.Messages;
using Wabbajack.Paths.IO; using Wabbajack.Paths.IO;
using Wabbajack.UserIntervention;
using Wabbajack.Util; using Wabbajack.Util;
using Wabbajack.Views; using Wabbajack.Views;
@ -26,12 +34,13 @@ namespace Wabbajack
private readonly ILogger<MainWindow> _logger; private readonly ILogger<MainWindow> _logger;
private readonly SystemParametersConstructor _systemParams; private readonly SystemParametersConstructor _systemParams;
private ObservableCollection<ViewModel> TabVMs = new ObservableCollectionExtended<ViewModel>();
public MainWindow(ILogger<MainWindow> logger, SystemParametersConstructor systemParams, LauncherUpdater updater, MainWindowVM vm) public MainWindow(ILogger<MainWindow> logger, SystemParametersConstructor systemParams, LauncherUpdater updater, MainWindowVM vm)
{ {
InitializeComponent(); InitializeComponent();
_mwvm = vm; _mwvm = vm;
DataContext = _mwvm; DataContext = vm;
_logger = logger; _logger = logger;
_systemParams = systemParams; _systemParams = systemParams;
try try
@ -45,6 +54,7 @@ namespace Wabbajack
}; };
MessageBus.Current.Listen<TaskBarUpdate>() MessageBus.Current.Listen<TaskBarUpdate>()
.ObserveOnGuiThread()
.Subscribe(u => .Subscribe(u =>
{ {
TaskbarItemInfo.Description = u.Description; TaskbarItemInfo.Description = u.Description;
@ -52,11 +62,6 @@ namespace Wabbajack
TaskbarItemInfo.ProgressState = u.State; TaskbarItemInfo.ProgressState = u.State;
}); });
MessageBus.Current.Listen<OpenBrowserTab>()
.Subscribe(OnOpenBrowserTab);
MessageBus.Current.Listen<CloseBrowserTab>()
.Subscribe(OnCloseBrowserTab);
_logger.LogInformation("Wabbajack Build - {Sha}",ThisAssembly.Git.Sha); _logger.LogInformation("Wabbajack Build - {Sha}",ThisAssembly.Git.Sha);
_logger.LogInformation("Running in {EntryPoint}", KnownFolders.EntryPoint); _logger.LogInformation("Running in {EntryPoint}", KnownFolders.EntryPoint);
@ -107,6 +112,7 @@ namespace Wabbajack
((MainWindowVM) DataContext).WhenAnyValue(vm => vm.OpenSettingsCommand) ((MainWindowVM) DataContext).WhenAnyValue(vm => vm.OpenSettingsCommand)
.BindTo(this, view => view.SettingsButton.Command); .BindTo(this, view => view.SettingsButton.Command);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -114,13 +120,10 @@ namespace Wabbajack
Environment.Exit(-1); Environment.Exit(-1);
} }
vm.WhenAnyValue(vm => vm.ResourceStatus) vm.WhenAnyValue(vm => vm.ResourceStatus)
.BindToStrict(this, view => view.ResourceUsage.Text); .BindToStrict(this, view => view.ResourceUsage.Text);
vm.WhenAnyValue(vm => vm.AppName)
vm.WhenAnyValue(vm => vm.ResourceStatus) .BindToStrict(this, view => view.AppName.Text);
.Select(x => string.IsNullOrWhiteSpace(x) ? Visibility.Collapsed : Visibility.Visible)
.BindToStrict(this, view => view.ResourceUsage.Visibility);
} }
@ -200,21 +203,5 @@ namespace Wabbajack
this.DragMove(); this.DragMove();
} }
private void OnOpenBrowserTab(OpenBrowserTab msg)
{
var tab = new BrowserTabView(msg.ViewModel);
Tabs.Items.Add(tab);
Tabs.SelectedItem = tab;
}
private void OnCloseBrowserTab(CloseBrowserTab msg)
{
foreach (var tab in Tabs.Items.OfType<BrowserTabView>())
{
if (tab.DataContext != msg.ViewModel) continue;
Tabs.Items.Remove(tab);
break;
}
}
} }
} }

View File

@ -0,0 +1,20 @@
<UserControl x:Class="Wabbajack.MainWindowContent"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Wabbajack"
xmlns:icon="http://metro.mahapps.com/winfx/xaml/iconpacks"
xmlns:viewModels="clr-namespace:Wabbajack.View_Models"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<!--
<TabItem>
<TabItem.Header>
</TabItem.Header>
</TabItem>-->
</UserControl>

View File

@ -0,0 +1,11 @@
using System.Windows.Controls;
namespace Wabbajack;
public partial class MainWindowContent : UserControl
{
public MainWindowContent()
{
InitializeComponent();
}
}

View File

@ -57,6 +57,10 @@ namespace Wabbajack
.BindToStrict(this, x => x.OpenWebsiteButton.Command) .BindToStrict(this, x => x.OpenWebsiteButton.Command)
.DisposeWith(disposables); .DisposeWith(disposables);
ViewModel.WhenAnyValue(x => x.ModListContentsCommend)
.BindToStrict(this, x => x.ModListContentsButton.Command)
.DisposeWith(disposables);
ViewModel.WhenAnyValue(x => x.ExecuteCommand) ViewModel.WhenAnyValue(x => x.ExecuteCommand)
.BindToStrict(this, x => x.ExecuteButton.Command) .BindToStrict(this, x => x.ExecuteButton.Command)
.DisposeWith(disposables); .DisposeWith(disposables);

View File

@ -1,9 +1,11 @@
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Wabbajack.DTOs.Interventions;
using Wabbajack.DTOs.Logins; using Wabbajack.DTOs.Logins;
using Wabbajack.Networking.Http; using Wabbajack.Networking.Http;
using Wabbajack.RateLimiter; using Wabbajack.RateLimiter;
@ -18,6 +20,15 @@ public static class HttpExtensions
return msg; return msg;
} }
public static HttpRequestMessage AddHeaders(this HttpRequestMessage msg, IEnumerable<(string Key, string Value)> headers)
{
foreach (var header in headers)
{
msg.Headers.Add(header.Key, header.Value);
}
return msg;
}
public static HttpRequestMessage AddChromeAgent(this HttpRequestMessage msg) public static HttpRequestMessage AddChromeAgent(this HttpRequestMessage msg)
{ {
msg.Headers.Add("User-Agent", msg.Headers.Add("User-Agent",
@ -25,6 +36,15 @@ public static class HttpExtensions
return msg; return msg;
} }
public static HttpRequestMessage ToHttpRequestMessage(this ManualDownload.BrowserDownloadState browserState)
{
var msg = new HttpRequestMessage(HttpMethod.Get, browserState.Uri);
msg.AddChromeAgent();
msg.AddCookies(browserState.Cookies);
msg.AddHeaders(browserState.Headers);
return msg;
}
public static async Task<TValue?> GetFromJsonAsync<TValue>(this HttpClient client, IResource<HttpClient> limiter, public static async Task<TValue?> GetFromJsonAsync<TValue>(this HttpClient client, IResource<HttpClient> limiter,
HttpRequestMessage msg, HttpRequestMessage msg,
JsonSerializerOptions? options, CancellationToken cancellationToken = default) JsonSerializerOptions? options, CancellationToken cancellationToken = default)

View File

@ -26,7 +26,7 @@ public class AUserIntervention<T> : IUserIntervention
public void Finish(T value) public void Finish(T value)
{ {
_tcs.SetResult(value); _tcs.TrySetResult(value);
_ct.Cancel(); _ct.Cancel();
} }

View File

@ -0,0 +1,16 @@
using Wabbajack.Hashing.xxHash64;
using Wabbajack.Paths;
namespace Wabbajack.DTOs.Interventions;
public class ManualBlobDownload : AUserIntervention<Hash>
{
public Archive Archive { get; }
public AbsolutePath Destination { get; }
public ManualBlobDownload(Archive archive, AbsolutePath destination)
{
Archive = archive;
Destination = destination;
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Wabbajack.DTOs.Logins;
using Wabbajack.Hashing.xxHash64;
using Wabbajack.Paths;
namespace Wabbajack.DTOs.Interventions;
public class ManualDownload : AUserIntervention<ManualDownload.BrowserDownloadState>
{
public Archive Archive { get; }
public ManualDownload(Archive archive)
{
Archive = archive;
}
public record BrowserDownloadState(Uri Uri, Cookie[] Cookies, (string Key, string Value)[] Headers)
{
}
}

View File

@ -1,20 +1,64 @@
using Wabbajack.Downloaders.Interfaces; using Microsoft.Extensions.Logging;
using Wabbajack.Common;
using Wabbajack.Downloaders.Interfaces;
using Wabbajack.DTOs; using Wabbajack.DTOs;
using Wabbajack.DTOs.DownloadStates; using Wabbajack.DTOs.DownloadStates;
using Wabbajack.DTOs.Interventions;
using Wabbajack.DTOs.Validation; using Wabbajack.DTOs.Validation;
using Wabbajack.Hashing.xxHash64; using Wabbajack.Hashing.xxHash64;
using Wabbajack.Paths; using Wabbajack.Paths;
using Wabbajack.Paths.IO;
using Wabbajack.RateLimiter; using Wabbajack.RateLimiter;
namespace Wabbajack.Downloaders.Manual; namespace Wabbajack.Downloaders.Manual;
public class ManualDownloader : ADownloader<DTOs.DownloadStates.Manual> public class ManualDownloader : ADownloader<DTOs.DownloadStates.Manual>
{ {
public override Task<Hash> Download(Archive archive, DTOs.DownloadStates.Manual state, AbsolutePath destination, IJob job, CancellationToken token) private readonly ILogger<ManualDownloader> _logger;
private readonly IUserInterventionHandler _interventionHandler;
private readonly IResource<HttpClient> _limiter;
private readonly HttpClient _client;
public ManualDownloader(ILogger<ManualDownloader> logger, IUserInterventionHandler interventionHandler, HttpClient client)
{ {
throw new NotImplementedException(); _logger = logger;
_interventionHandler = interventionHandler;
_client = client;
} }
public override async Task<Hash> Download(Archive archive, DTOs.DownloadStates.Manual state, AbsolutePath destination, IJob job, CancellationToken token)
{
_logger.LogInformation("Starting manual download of {Url}", state.Url);
if (state.Url.Host == "mega.nz")
{
var intervention = new ManualBlobDownload(archive, destination);
_interventionHandler.Raise(intervention);
await intervention.Task;
if (!destination.FileExists())
throw new Exception("File does not exist after download");
_logger.LogInformation("Hashing manually downloaded Mega file {File}", destination.FileName);
return await destination.Hash(token);
}
else
{
var intervention = new ManualDownload(archive);
_interventionHandler.Raise(intervention);
var browserState = await intervention.Task;
var msg = browserState.ToHttpRequestMessage();
using var response = await _client.SendAsync(msg, token);
if (!response.IsSuccessStatusCode)
throw new HttpRequestException(response.ReasonPhrase, null, statusCode: response.StatusCode);
await using var strm = await response.Content.ReadAsStreamAsync(token);
await using var os = destination.Open(FileMode.Create, FileAccess.Write, FileShare.None);
return await strm.HashingCopy(os, token, job);
}
}
public override async Task<bool> Prepare() public override async Task<bool> Prepare()
{ {
return true; return true;

View File

@ -7,7 +7,12 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Wabbajack.Common\Wabbajack.Common.csproj" />
<ProjectReference Include="..\Wabbajack.Downloaders.Interfaces\Wabbajack.Downloaders.Interfaces.csproj" /> <ProjectReference Include="..\Wabbajack.Downloaders.Interfaces\Wabbajack.Downloaders.Interfaces.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.2-mauipre.1.22054.8" />
</ItemGroup>
</Project> </Project>

View File

@ -1,20 +1,25 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Web; using System.Web;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Wabbajack.Common;
using Wabbajack.Downloaders.Interfaces; using Wabbajack.Downloaders.Interfaces;
using Wabbajack.DTOs; using Wabbajack.DTOs;
using Wabbajack.DTOs.DownloadStates; using Wabbajack.DTOs.DownloadStates;
using Wabbajack.DTOs.Interventions;
using Wabbajack.DTOs.Validation; using Wabbajack.DTOs.Validation;
using Wabbajack.Hashing.xxHash64; using Wabbajack.Hashing.xxHash64;
using Wabbajack.Networking.Http; using Wabbajack.Networking.Http;
using Wabbajack.Networking.Http.Interfaces; using Wabbajack.Networking.Http.Interfaces;
using Wabbajack.Networking.NexusApi; using Wabbajack.Networking.NexusApi;
using Wabbajack.Paths; using Wabbajack.Paths;
using Wabbajack.Paths.IO;
using Wabbajack.RateLimiter; using Wabbajack.RateLimiter;
namespace Wabbajack.Downloaders; namespace Wabbajack.Downloaders;
@ -25,14 +30,20 @@ public class NexusDownloader : ADownloader<Nexus>, IUrlDownloader
private readonly HttpClient _client; private readonly HttpClient _client;
private readonly IHttpDownloader _downloader; private readonly IHttpDownloader _downloader;
private readonly ILogger<NexusDownloader> _logger; private readonly ILogger<NexusDownloader> _logger;
private readonly IUserInterventionHandler _userInterventionHandler;
private readonly IResource<IUserInterventionHandler> _interventionLimiter;
private const bool IsManualDebugMode = true;
public NexusDownloader(ILogger<NexusDownloader> logger, HttpClient client, IHttpDownloader downloader, public NexusDownloader(ILogger<NexusDownloader> logger, HttpClient client, IHttpDownloader downloader,
NexusApi api) NexusApi api, IUserInterventionHandler userInterventionHandler, IResource<IUserInterventionHandler> interventionLimiter)
{ {
_logger = logger; _logger = logger;
_client = client; _client = client;
_downloader = downloader; _downloader = downloader;
_api = api; _api = api;
_userInterventionHandler = userInterventionHandler;
_interventionLimiter = interventionLimiter;
} }
public override async Task<bool> Prepare() public override async Task<bool> Prepare()
@ -104,11 +115,64 @@ public class NexusDownloader : ADownloader<Nexus>, IUrlDownloader
public override async Task<Hash> Download(Archive archive, Nexus state, AbsolutePath destination, public override async Task<Hash> Download(Archive archive, Nexus state, AbsolutePath destination,
IJob job, CancellationToken token) IJob job, CancellationToken token)
{ {
var urls = await _api.DownloadLink(state.Game.MetaData().NexusName!, state.ModID, state.FileID, token); if (IsManualDebugMode || await _api.IsPremium(token))
_logger.LogInformation("Downloading Nexus File: {game}|{modid}|{fileid}", state.Game, state.ModID, {
state.FileID); return await DownloadManually(archive, state, destination, job, token);
var message = new HttpRequestMessage(HttpMethod.Get, urls.info.First().URI); }
return await _downloader.Download(message, destination, job, token); else
{
try
{
var urls = await _api.DownloadLink(state.Game.MetaData().NexusName!, state.ModID, state.FileID, token);
_logger.LogInformation("Downloading Nexus File: {game}|{modid}|{fileid}", state.Game, state.ModID,
state.FileID);
var message = new HttpRequestMessage(HttpMethod.Get, urls.info.First().URI);
return await _downloader.Download(message, destination, job, token);
}
catch (HttpRequestException ex)
{
if (ex.StatusCode == HttpStatusCode.Forbidden)
{
return await DownloadManually(archive, state, destination, job, token);
}
throw;
}
}
}
private async Task<Hash> DownloadManually(Archive archive, Nexus state, AbsolutePath destination, IJob job, CancellationToken token)
{
var md = new ManualDownload(new Archive
{
Name = archive.Name,
Hash = archive.Hash,
Meta = archive.Meta,
Size = archive.Size,
State = new Manual
{
Prompt = "Click Download - Buy Nexus Premium to automate this process",
Url = new Uri($"https://www.nexusmods.com/{state.Game.MetaData().NexusName}/mods/{state.ModID}?tab=files&file_id={state.FileID}")
}
});
ManualDownload.BrowserDownloadState browserState;
using (var _ = await _interventionLimiter.Begin("Downloading file manually", 1, token))
{
_userInterventionHandler.Raise(md);
browserState = await md.Task;
}
var msg = browserState.ToHttpRequestMessage();
using var response = await _client.SendAsync(msg, HttpCompletionOption.ResponseHeadersRead, token);
if (!response.IsSuccessStatusCode)
throw new HttpRequestException(response.ReasonPhrase, null, statusCode:response.StatusCode);
await using var strm = await response.Content.ReadAsStreamAsync(token);
await using var os = destination.Open(FileMode.Create, FileAccess.Write, FileShare.None);
return await strm.HashingCopy(os, token, job);
} }
public override async Task<bool> Verify(Archive archive, Nexus state, IJob job, CancellationToken token) public override async Task<bool> Verify(Archive archive, Nexus state, IJob job, CancellationToken token)

View File

@ -6,6 +6,7 @@ using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Wabbajack.Common;
using Wabbajack.Downloaders.Interfaces; using Wabbajack.Downloaders.Interfaces;
using Wabbajack.DTOs; using Wabbajack.DTOs;
using Wabbajack.DTOs.CDN; using Wabbajack.DTOs.CDN;
@ -33,12 +34,14 @@ public class WabbajackCDNDownloader : ADownloader<WabbajackCDN>, IUrlDownloader,
private readonly DTOSerializer _dtos; private readonly DTOSerializer _dtos;
private readonly ILogger<WabbajackCDNDownloader> _logger; private readonly ILogger<WabbajackCDNDownloader> _logger;
private readonly ParallelOptions _parallelOptions; private readonly ParallelOptions _parallelOptions;
private readonly IResource<HttpClient> _limiter;
public WabbajackCDNDownloader(ILogger<WabbajackCDNDownloader> logger, HttpClient client, DTOSerializer dtos) public WabbajackCDNDownloader(ILogger<WabbajackCDNDownloader> logger, HttpClient client, IResource<HttpClient> limiter, DTOSerializer dtos)
{ {
_client = client; _client = client;
_logger = logger; _logger = logger;
_dtos = dtos; _dtos = dtos;
_limiter = limiter;
} }
public override Task<bool> Prepare() public override Task<bool> Prepare()
@ -78,8 +81,11 @@ public class WabbajackCDNDownloader : ADownloader<WabbajackCDN>, IUrlDownloader,
var definition = (await GetDefinition(state, token))!; var definition = (await GetDefinition(state, token))!;
await using var fs = destination.Open(FileMode.Create, FileAccess.Write, FileShare.None); await using var fs = destination.Open(FileMode.Create, FileAccess.Write, FileShare.None);
foreach (var part in definition.Parts) await definition.Parts.PMapAll(async part =>
{ {
using var partJob = await _limiter.Begin(
$"Downloading {definition.MungedName} ({part.Index}/{definition.Size})",
part.Size, token);
var msg = MakeMessage(new Uri(state.Url + $"/parts/{part.Index}")); var msg = MakeMessage(new Uri(state.Url + $"/parts/{part.Index}"));
using var response = await _client.SendAsync(msg, HttpCompletionOption.ResponseHeadersRead, token); using var response = await _client.SendAsync(msg, HttpCompletionOption.ResponseHeadersRead, token);
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
@ -92,12 +98,26 @@ public class WabbajackCDNDownloader : ADownloader<WabbajackCDN>, IUrlDownloader,
await using var data = await response.Content.ReadAsStreamAsync(token); await using var data = await response.Content.ReadAsStreamAsync(token);
fs.Position = part.Offset; var ms = new MemoryStream();
var hash = await data.HashingCopy(fs, token, job); var hash = await data.HashingCopy(ms, token, partJob);
ms.Position = 0;
if (hash != part.Hash) if (hash != part.Hash)
throw new InvalidDataException($"Bad part hash, got {hash} expected {part.Hash} for part {part.Index}"); {
throw new Exception(
$"Invalid part hash {part.Index} got {hash} instead of {part.Hash} for {definition.MungedName}");
}
return (ms, part);
}).Do(async rec =>
{
var (ms, part) = rec;
fs.Position = part.Offset;
await job.Report((int)part.Size, token);
await ms.CopyToAsync(fs, token);
await fs.FlushAsync(token); await fs.FlushAsync(token);
} });
return definition.Hash; return definition.Hash;
} }

View File

@ -321,7 +321,7 @@ public class FileExtractor
process.Arguments = new object[] {"x", "-bsp1", "-y", $"-o\"{dest}\"", source, "-mmt=off"}; process.Arguments = new object[] {"x", "-bsp1", "-y", $"-o\"{dest}\"", source, "-mmt=off"};
} }
_logger.LogInformation("{prog} {args}", process.Path, process.Arguments); _logger.LogTrace("{prog} {args}", process.Path, process.Arguments);
var totalSize = source.Size(); var totalSize = source.Size();
var lastPercent = 0; var lastPercent = 0;

View File

@ -39,12 +39,13 @@ public abstract class AInstaller<T>
where T : AInstaller<T> where T : AInstaller<T>
{ {
private const int _limitMS = 100; private const int _limitMS = 100;
public static RelativePath BSACreationDir = "TEMP_BSA_FILES".ToRelativePath();
private static readonly Regex NoDeleteRegex = new(@"(?i)[\\\/]\[NoDelete\]", RegexOptions.Compiled); private static readonly Regex NoDeleteRegex = new(@"(?i)[\\\/]\[NoDelete\]", RegexOptions.Compiled);
protected readonly InstallerConfiguration _configuration; protected readonly InstallerConfiguration _configuration;
protected readonly DownloadDispatcher _downloadDispatcher; protected readonly DownloadDispatcher _downloadDispatcher;
private readonly FileExtractor.FileExtractor _extractor; private readonly FileExtractor.FileExtractor _extractor;
private readonly FileHashCache _fileHashCache; protected readonly FileHashCache FileHashCache;
protected readonly IGameLocator _gameLocator; protected readonly IGameLocator _gameLocator;
private readonly DTOSerializer _jsonSerializer; private readonly DTOSerializer _jsonSerializer;
protected readonly ILogger<T> _logger; protected readonly ILogger<T> _logger;
@ -62,14 +63,18 @@ public abstract class AInstaller<T>
private readonly Stopwatch _updateStopWatch = new(); private readonly Stopwatch _updateStopWatch = new();
public Action<StatusUpdate>? OnStatusUpdate; public Action<StatusUpdate>? OnStatusUpdate;
private readonly IResource<IInstaller> _limiter;
public AInstaller(ILogger<T> logger, InstallerConfiguration config, IGameLocator gameLocator, public AInstaller(ILogger<T> logger, InstallerConfiguration config, IGameLocator gameLocator,
FileExtractor.FileExtractor extractor, FileExtractor.FileExtractor extractor,
DTOSerializer jsonSerializer, Context vfs, FileHashCache fileHashCache, DTOSerializer jsonSerializer, Context vfs, FileHashCache fileHashCache,
DownloadDispatcher downloadDispatcher, DownloadDispatcher downloadDispatcher,
ParallelOptions parallelOptions, Client wjClient) ParallelOptions parallelOptions,
IResource<IInstaller> limiter,
Client wjClient)
{ {
_limiter = limiter;
_manager = new TemporaryFileManager(config.Install.Combine("__temp__")); _manager = new TemporaryFileManager(config.Install.Combine("__temp__"));
ExtractedModlistFolder = _manager.CreateFolder(); ExtractedModlistFolder = _manager.CreateFolder();
_configuration = config; _configuration = config;
@ -77,7 +82,7 @@ public abstract class AInstaller<T>
_extractor = extractor; _extractor = extractor;
_jsonSerializer = jsonSerializer; _jsonSerializer = jsonSerializer;
_vfs = vfs; _vfs = vfs;
_fileHashCache = fileHashCache; FileHashCache = fileHashCache;
_downloadDispatcher = downloadDispatcher; _downloadDispatcher = downloadDispatcher;
_parallelOptions = parallelOptions; _parallelOptions = parallelOptions;
_gameLocator = gameLocator; _gameLocator = gameLocator;
@ -110,7 +115,9 @@ public abstract class AInstaller<T>
{ {
Interlocked.Add(ref _currentStepProgress, stepProgress); Interlocked.Add(ref _currentStepProgress, stepProgress);
OnStatusUpdate?.Invoke(new StatusUpdate(_statusCategory, _statusText, Percent.FactoryPutInRange(_currentStep, MaxSteps), Percent.FactoryPutInRange(_currentStepProgress, MaxStepProgress))); OnStatusUpdate?.Invoke(new StatusUpdate(_statusCategory, $"[{_currentStep}/{MaxSteps}] {_statusText} ({_currentStepProgress}/{MaxStepProgress})",
Percent.FactoryPutInRange(_currentStep, MaxSteps),
Percent.FactoryPutInRange(_currentStepProgress, MaxStepProgress)));
} }
public abstract Task<bool> Begin(CancellationToken token); public abstract Task<bool> Begin(CancellationToken token);
@ -120,7 +127,7 @@ public abstract class AInstaller<T>
ExtractedModlistFolder = _manager.CreateFolder(); ExtractedModlistFolder = _manager.CreateFolder();
await using var stream = _configuration.ModlistArchive.Open(FileMode.Open, FileAccess.Read, FileShare.Read); await using var stream = _configuration.ModlistArchive.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
using var archive = new ZipArchive(stream, ZipArchiveMode.Read); using var archive = new ZipArchive(stream, ZipArchiveMode.Read);
NextStep(Consts.StepPreparing,"Extracting Modlist", archive.Entries.Count); NextStep(Consts.StepPreparing, "Extracting Modlist", archive.Entries.Count);
foreach (var entry in archive.Entries) foreach (var entry in archive.Entries)
{ {
var path = entry.FullName.ToRelativePath().RelativeTo(ExtractedModlistFolder); var path = entry.FullName.ToRelativePath().RelativeTo(ExtractedModlistFolder);
@ -182,7 +189,7 @@ public abstract class AInstaller<T>
/// </summary> /// </summary>
protected async Task PrimeVFS() protected async Task PrimeVFS()
{ {
NextStep(Consts.StepPreparing,"Priming VFS", 0); NextStep(Consts.StepPreparing, "Priming VFS", 0);
_vfs.AddKnown(_configuration.ModList.Directives.OfType<FromArchive>().Select(d => d.ArchiveHashPath), _vfs.AddKnown(_configuration.ModList.Directives.OfType<FromArchive>().Select(d => d.ArchiveHashPath),
HashedArchives); HashedArchives);
await _vfs.BackfillMissing(); await _vfs.BackfillMissing();
@ -210,9 +217,13 @@ public abstract class AInstaller<T>
if (grouped.Count == 0) return; if (grouped.Count == 0) return;
await _vfs.Extract(grouped.Keys.ToHashSet(), async (vf, sf) => await _vfs.Extract(grouped.Keys.ToHashSet(), async (vf, sf) =>
{ {
foreach (var directive in grouped[vf]) var directives = grouped[vf];
using var job = await _limiter.Begin($"Installing files from {vf.Name}", directives.Sum(f => f.VF.Size),
token);
foreach (var directive in directives)
{ {
var file = directive.Directive; var file = directive.Directive;
UpdateProgress(file.Size); UpdateProgress(file.Size);
@ -260,6 +271,8 @@ public abstract class AInstaller<T>
default: default:
throw new Exception($"No handler for {directive}"); throw new Exception($"No handler for {directive}");
} }
await job.Report((int) directive.VF.Size, token);
} }
}, token); }, token);
} }
@ -280,7 +293,7 @@ public abstract class AInstaller<T>
_logger.LogInformation("Validating Archives"); _logger.LogInformation("Validating Archives");
foreach (var archive in missing.Where(archive => foreach (var archive in missing.Where(archive =>
!_downloadDispatcher.Downloader(archive).IsAllowed(validationData, archive.State))) !_downloadDispatcher.Downloader(archive).IsAllowed(validationData, archive.State)))
{ {
_logger.LogCritical("File {primaryKeyString} failed validation", archive.State.PrimaryKeyString); _logger.LogCritical("File {primaryKeyString} failed validation", archive.State.PrimaryKeyString);
return; return;
@ -292,19 +305,20 @@ public abstract class AInstaller<T>
public async Task DownloadMissingArchives(List<Archive> missing, CancellationToken token, bool download = true) public async Task DownloadMissingArchives(List<Archive> missing, CancellationToken token, bool download = true)
{ {
_logger.LogInformation("Downloading {Count} archives", missing.Count.ToString());
NextStep(Consts.StepDownloading, "Downloading files", missing.Count);
if (download) if (download)
{ {
var result = SendDownloadMetrics(missing); var result = SendDownloadMetrics(missing);
foreach (var a in missing.Where(a => a.State is Manual)) foreach (var a in missing.Where(a => a.State is Manual))
{ {
var outputPath = _configuration.Downloads.Combine(a.Name); var outputPath = _configuration.Downloads.Combine(a.Name);
await _downloadDispatcher.Download(a, outputPath, token); await DownloadArchive(a, true, token, outputPath);
UpdateProgress(1);
} }
} }
_logger.LogInformation("Downloading {Count} archives", missing.Count.ToString());
NextStep(Consts.StepDownloading, "Downloading files", missing.Count);
await missing await missing
.OrderBy(a => a.Size) .OrderBy(a => a.Size)
.Where(a => a.State is not Manual) .Where(a => a.State is not Manual)
@ -323,7 +337,7 @@ public abstract class AInstaller<T>
outputPath.Delete(); outputPath.Delete();
} }
await DownloadArchive(archive, download, token, outputPath); var hash = await DownloadArchive(archive, download, token, outputPath);
UpdateProgress(1); UpdateProgress(1);
}); });
} }
@ -346,8 +360,20 @@ public abstract class AInstaller<T>
var (result, hash) = var (result, hash) =
await _downloadDispatcher.DownloadWithPossibleUpgrade(archive, destination.Value, token); await _downloadDispatcher.DownloadWithPossibleUpgrade(archive, destination.Value, token);
if (hash != archive.Hash)
{
_logger.LogError("Downloaded hash {Downloaded} does not match expected hash: {Expected}", hash,
archive.Hash);
if (destination!.Value.FileExists())
{
destination!.Value.Delete();
}
return false;
}
if (hash != default) if (hash != default)
_fileHashCache.FileHashWriteCache(destination.Value, hash); FileHashCache.FileHashWriteCache(destination.Value, hash);
if (result == DownloadResult.Update) if (result == DownloadResult.Update)
await destination.Value.MoveToAsync(destination.Value.Parent.Combine(archive.Hash.ToHex()), true, await destination.Value.MoveToAsync(destination.Value.Parent.Combine(archive.Hash.ToHex()), true,
@ -386,7 +412,7 @@ public abstract class AInstaller<T>
.PMapAll(async e => .PMapAll(async e =>
{ {
UpdateProgress(1); UpdateProgress(1);
return (await _fileHashCache.FileHashCachedAsync(e, token), e); return (await FileHashCache.FileHashCachedAsync(e, token), e);
}) })
.ToList(); .ToList();
@ -407,19 +433,46 @@ public abstract class AInstaller<T>
{ {
_logger.LogInformation("Optimizing ModList directives"); _logger.LogInformation("Optimizing ModList directives");
var indexed = ModList.Directives.ToDictionary(d => d.To); var indexed = ModList.Directives.ToDictionary(d => d.To);
var bsasToBuild = await ModList.Directives
.OfType<CreateBSA>()
.PMapAll(async b =>
{
var file = _configuration.Install.Combine(b.To);
if (!file.FileExists())
return (true, b);
return (b.Hash != await FileHashCache.FileHashCachedAsync(file, token), b);
})
.ToArray();
var bsasToNotBuild = bsasToBuild
.Where(b => b.Item1 == false).Select(t => t.b.TempID).ToHashSet();
var bsaPathsToNotBuild = bsasToBuild
.Where(b => b.Item1 == false).Select(t => t.b.To.RelativeTo(_configuration.Install))
.ToHashSet();
indexed = indexed.Values
.Where(d =>
{
return d switch
{
CreateBSA bsa => !bsasToNotBuild.Contains(bsa.TempID),
FromArchive a when a.To.StartsWith($"{BSACreationDir}") => !bsasToNotBuild.Any(b =>
a.To.RelativeTo(_configuration.Install).InFolder(_configuration.Install.Combine(BSACreationDir, b))),
_ => true
};
}).ToDictionary(d => d.To);
var profileFolder = _configuration.Install.Combine("profiles"); var profileFolder = _configuration.Install.Combine("profiles");
var savePath = (RelativePath) "saves"; var savePath = (RelativePath) "saves";
var existingFiles = _configuration.Install.EnumerateFiles().ToList(); NextStep(Consts.StepPreparing, "Looking for files to delete", 0);
NextStep(Consts.StepPreparing, "Looking for files to delete", existingFiles.Count); await _configuration.Install.EnumerateFiles()
await existingFiles
.PDoAll(async f => .PDoAll(async f =>
{ {
UpdateProgress(1);
var relativeTo = f.RelativeTo(_configuration.Install); var relativeTo = f.RelativeTo(_configuration.Install);
if (indexed.ContainsKey(relativeTo) || f.InFolder(_configuration.Downloads)) if (indexed.ContainsKey(relativeTo) || f.InFolder(_configuration.Downloads))
return; return;
@ -429,47 +482,50 @@ public abstract class AInstaller<T>
if (NoDeleteRegex.IsMatch(f.ToString())) if (NoDeleteRegex.IsMatch(f.ToString()))
return; return;
_logger.LogTrace("Deleting {relativeTo} it's not part of this ModList", relativeTo); if (bsaPathsToNotBuild.Contains(f))
return;
_logger.LogInformation("Deleting {RelativePath} it's not part of this ModList", relativeTo);
f.Delete(); f.Delete();
}); });
_logger.LogInformation("Cleaning empty folders"); _logger.LogInformation("Cleaning empty folders");
NextStep(Consts.StepPreparing, "Cleaning empty folders", indexed.Keys.Count); var expectedFolders = indexed.Keys
var expectedFolders = (indexed.Keys .Select(f => f.RelativeTo(_configuration.Install))
.Select(f => f.RelativeTo(_configuration.Install)) // We ignore the last part of the path, so we need a dummy file name
// We ignore the last part of the path, so we need a dummy file name .Append(_configuration.Downloads.Combine("_"))
.Append(_configuration.Downloads.Combine("_")) .Where(f => f.InFolder(_configuration.Install))
.OnEach(_ => UpdateProgress(1)) .SelectMany(path =>
.Where(f => f.InFolder(_configuration.Install)) {
.SelectMany(path => // Get all the folders and all the folder parents
{ // so for foo\bar\baz\qux.txt this emits ["foo", "foo\\bar", "foo\\bar\\baz"]
// Get all the folders and all the folder parents var split = ((string) path.RelativeTo(_configuration.Install)).Split('\\');
// so for foo\bar\baz\qux.txt this emits ["foo", "foo\\bar", "foo\\bar\\baz"] return Enumerable.Range(1, split.Length - 1).Select(t => string.Join("\\", split.Take(t)));
var split = ((string) path.RelativeTo(_configuration.Install)).Split('\\'); })
return Enumerable.Range(1, split.Length - 1).Select(t => string.Join("\\", split.Take(t)));
})
.ToList())
.Distinct() .Distinct()
.Select(p => _configuration.Install.Combine(p)) .Select(p => _configuration.Install.Combine(p))
.ToHashSet(); .ToHashSet();
try try
{ {
var toDelete = _configuration.Install.EnumerateDirectories() var toDelete = _configuration.Install.EnumerateDirectories(true)
.Where(p => !expectedFolders.Contains(p)) .Where(p => !expectedFolders.Contains(p))
.OrderByDescending(p => p.ToString().Length) .OrderByDescending(p => p.ToString().Length)
.ToList(); .ToList();
foreach (var dir in toDelete) dir.DeleteDirectory(true); foreach (var dir in toDelete)
{
dir.DeleteDirectory(dontDeleteIfNotEmpty: true);
}
} }
catch (Exception) catch (Exception)
{ {
// ignored because it's not worth throwing a fit over // ignored because it's not worth throwing a fit over
_logger.LogWarning("Error when trying to clean empty folders. This doesn't really matter."); _logger.LogInformation("Error when trying to clean empty folders. This doesn't really matter.");
} }
var existingfiles = _configuration.Install.EnumerateFiles().ToHashSet(); var existingfiles = _configuration.Install.EnumerateFiles().ToHashSet();
NextStep(Consts.StepPreparing, "Removing redundant directives", indexed.Count); NextStep(Consts.StepPreparing, "Looking for unmodified files", 0);
await indexed.Values.PMapAll<Directive, Directive?>(async d => await indexed.Values.PMapAll<Directive, Directive?>(async d =>
{ {
// Bit backwards, but we want to return null for // Bit backwards, but we want to return null for
@ -478,17 +534,18 @@ public abstract class AInstaller<T>
var path = _configuration.Install.Combine(d.To); var path = _configuration.Install.Combine(d.To);
if (!existingfiles.Contains(path)) return null; if (!existingfiles.Contains(path)) return null;
return await _fileHashCache.FileHashCachedAsync(path, token) == d.Hash ? d : null; return await FileHashCache.FileHashCachedAsync(path, token) == d.Hash ? d : null;
}) })
.Do(d => .Do(d =>
{ {
UpdateProgress(1); if (d != null)
if (d != null) indexed.Remove(d.To); {
indexed.Remove(d.To);
}
}); });
_logger.LogInformation("Optimized {optimized} directives to {indexed} required", ModList.Directives.Length, NextStep(Consts.StepPreparing, "Updating ModList", 0);
indexed.Count); _logger.LogInformation("Optimized {From} directives to {To} required", ModList.Directives.Length, indexed.Count);
NextStep(Consts.StepPreparing, "Finalizing modlist optimization", 0);
var requiredArchives = indexed.Values.OfType<FromArchive>() var requiredArchives = indexed.Values.OfType<FromArchive>()
.GroupBy(d => d.ArchiveHashPath.Hash) .GroupBy(d => d.ArchiveHashPath.Hash)
.Select(d => d.Key) .Select(d => d.Key)

View File

@ -26,21 +26,21 @@ using Wabbajack.Installer.Utilities;
using Wabbajack.Networking.WabbajackClientApi; using Wabbajack.Networking.WabbajackClientApi;
using Wabbajack.Paths; using Wabbajack.Paths;
using Wabbajack.Paths.IO; using Wabbajack.Paths.IO;
using Wabbajack.RateLimiter;
using Wabbajack.VFS; using Wabbajack.VFS;
namespace Wabbajack.Installer; namespace Wabbajack.Installer;
public class StandardInstaller : AInstaller<StandardInstaller> public class StandardInstaller : AInstaller<StandardInstaller>
{ {
public static RelativePath BSACreationDir = "TEMP_BSA_FILES".ToRelativePath();
public StandardInstaller(ILogger<StandardInstaller> logger, public StandardInstaller(ILogger<StandardInstaller> logger,
InstallerConfiguration config, InstallerConfiguration config,
IGameLocator gameLocator, FileExtractor.FileExtractor extractor, IGameLocator gameLocator, FileExtractor.FileExtractor extractor,
DTOSerializer jsonSerializer, Context vfs, FileHashCache fileHashCache, DTOSerializer jsonSerializer, Context vfs, FileHashCache fileHashCache,
DownloadDispatcher downloadDispatcher, ParallelOptions parallelOptions, Client wjClient) : DownloadDispatcher downloadDispatcher, ParallelOptions parallelOptions, IResource<IInstaller> limiter, Client wjClient) :
base(logger, config, gameLocator, extractor, jsonSerializer, vfs, fileHashCache, downloadDispatcher, base(logger, config, gameLocator, extractor, jsonSerializer, vfs, fileHashCache, downloadDispatcher,
parallelOptions, wjClient) parallelOptions, limiter, wjClient)
{ {
MaxSteps = 14; MaxSteps = 14;
} }
@ -56,6 +56,7 @@ public class StandardInstaller : AInstaller<StandardInstaller>
provider.GetRequiredService<FileHashCache>(), provider.GetRequiredService<FileHashCache>(),
provider.GetRequiredService<DownloadDispatcher>(), provider.GetRequiredService<DownloadDispatcher>(),
provider.GetRequiredService<ParallelOptions>(), provider.GetRequiredService<ParallelOptions>(),
provider.GetRequiredService<IResource<IInstaller>>(),
provider.GetRequiredService<Client>()); provider.GetRequiredService<Client>());
} }
@ -258,11 +259,14 @@ public class StandardInstaller : AInstaller<StandardInstaller>
}).ToList(); }).ToList();
_logger.LogInformation("Writing {bsaTo}", bsa.To); _logger.LogInformation("Writing {bsaTo}", bsa.To);
await using var outStream = _configuration.Install.Combine(bsa.To) var outPath = _configuration.Install.Combine(bsa.To);
.Open(FileMode.Create, FileAccess.Write, FileShare.None); await using var outStream = outPath.Open(FileMode.Create, FileAccess.Write, FileShare.None);
await a.Build(outStream, token); await a.Build(outStream, token);
streams.Do(s => s.Dispose()); streams.Do(s => s.Dispose());
FileHashCache.FileHashWriteCache(outPath, bsa.Hash);
sourceDir.DeleteDirectory(); sourceDir.DeleteDirectory();
} }

View File

@ -30,6 +30,8 @@ public class NexusApi
private readonly IResource<HttpClient> _limiter; private readonly IResource<HttpClient> _limiter;
private readonly ILogger<NexusApi> _logger; private readonly ILogger<NexusApi> _logger;
protected readonly ITokenProvider<NexusApiState> ApiKey; protected readonly ITokenProvider<NexusApiState> ApiKey;
private DateTime _lastValidated;
private (ValidateInfo info, ResponseMetadata header) _lastValidatedInfo;
public NexusApi(ITokenProvider<NexusApiState> apiKey, ILogger<NexusApi> logger, HttpClient client, public NexusApi(ITokenProvider<NexusApiState> apiKey, ILogger<NexusApi> logger, HttpClient client,
IResource<HttpClient> limiter, IResource<HttpClient> limiter,
@ -41,6 +43,8 @@ public class NexusApi
_appInfo = appInfo; _appInfo = appInfo;
_jsonOptions = jsonOptions; _jsonOptions = jsonOptions;
_limiter = limiter; _limiter = limiter;
_lastValidated = DateTime.MinValue;
_lastValidatedInfo = default;
} }
public virtual async Task<(ValidateInfo info, ResponseMetadata header)> Validate( public virtual async Task<(ValidateInfo info, ResponseMetadata header)> Validate(
@ -50,6 +54,19 @@ public class NexusApi
return await Send<ValidateInfo>(msg, token); return await Send<ValidateInfo>(msg, token);
} }
public async Task<(ValidateInfo info, ResponseMetadata header)> ValidateCached(
CancellationToken token = default)
{
if (DateTime.Now - _lastValidated < TimeSpan.FromMinutes(10))
{
return _lastValidatedInfo;
}
var msg = await GenerateMessage(HttpMethod.Get, Endpoints.Validate);
_lastValidatedInfo = await Send<ValidateInfo>(msg, token);
return _lastValidatedInfo;
}
public virtual async Task<(ModInfo info, ResponseMetadata header)> ModInfo(string nexusGameName, long modId, public virtual async Task<(ModInfo info, ResponseMetadata header)> ModInfo(string nexusGameName, long modId,
CancellationToken token = default) CancellationToken token = default)
{ {
@ -310,4 +327,10 @@ public class NexusApi
await Task.Delay(TimeSpan.FromSeconds(5)); await Task.Delay(TimeSpan.FromSeconds(5));
} }
} }
public async Task<bool> IsPremium(CancellationToken token)
{
var validated = await ValidateCached(token);
return validated.info.IsPremium;
}
} }

View File

@ -178,4 +178,9 @@ public readonly struct RelativePath : IPath, IEquatable<RelativePath>, IComparab
{ {
return Parts[^1].StartsWith(mrkinn); return Parts[^1].StartsWith(mrkinn);
} }
public bool StartsWith(string s)
{
return ToString().StartsWith(s);
}
} }

View File

@ -9,6 +9,7 @@ using Wabbajack.Compiler;
using Wabbajack.Downloaders; using Wabbajack.Downloaders;
using Wabbajack.Downloaders.GameFile; using Wabbajack.Downloaders.GameFile;
using Wabbajack.DTOs; using Wabbajack.DTOs;
using Wabbajack.DTOs.Interventions;
using Wabbajack.DTOs.Logins; using Wabbajack.DTOs.Logins;
using Wabbajack.Installer; using Wabbajack.Installer;
using Wabbajack.Networking.BethesdaNet; using Wabbajack.Networking.BethesdaNet;
@ -97,6 +98,12 @@ public static class ServiceExtensions
service.AddAllSingleton<IResource, IResource<ACompiler>>(s => service.AddAllSingleton<IResource, IResource<ACompiler>>(s =>
new Resource<ACompiler>("Compiler", GetSettings(s, "Compiler"))); new Resource<ACompiler>("Compiler", GetSettings(s, "Compiler")));
service.AddAllSingleton<IResource, IResource<IInstaller>>(s =>
new Resource<IInstaller>("Installer", GetSettings(s, "Installer")));
service.AddAllSingleton<IResource, IResource<IUserInterventionHandler>>(s =>
new Resource<IUserInterventionHandler>("User Intervention", 1));
service.AddSingleton<LoggingRateLimiterReporter>(); service.AddSingleton<LoggingRateLimiterReporter>();
service.AddScoped<Context>(); service.AddScoped<Context>();

View File

@ -1,11 +1,13 @@
using System; using System;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Wabbajack.Common; using Wabbajack.Common;
using Wabbajack.Downloaders; using Wabbajack.Downloaders;
using Wabbajack.DTOs; using Wabbajack.DTOs;
using Wabbajack.DTOs.JsonConverters;
using Wabbajack.Paths; using Wabbajack.Paths;
using Wabbajack.Paths.IO; using Wabbajack.Paths.IO;
using Wabbajack.RateLimiter; using Wabbajack.RateLimiter;
@ -21,9 +23,10 @@ public class ModListDownloadMaintainer
private readonly FileHashCache _hashCache; private readonly FileHashCache _hashCache;
private readonly IResource<DownloadDispatcher> _rateLimiter; private readonly IResource<DownloadDispatcher> _rateLimiter;
private int _downloadingCount; private int _downloadingCount;
private readonly DTOSerializer _dtos;
public ModListDownloadMaintainer(ILogger<ModListDownloadMaintainer> logger, Configuration configuration, public ModListDownloadMaintainer(ILogger<ModListDownloadMaintainer> logger, Configuration configuration,
DownloadDispatcher dispatcher, FileHashCache hashCache, IResource<DownloadDispatcher> rateLimiter) DownloadDispatcher dispatcher, FileHashCache hashCache, DTOSerializer dtos, IResource<DownloadDispatcher> rateLimiter)
{ {
_logger = logger; _logger = logger;
_configuration = configuration; _configuration = configuration;
@ -31,6 +34,7 @@ public class ModListDownloadMaintainer
_hashCache = hashCache; _hashCache = hashCache;
_rateLimiter = rateLimiter; _rateLimiter = rateLimiter;
_downloadingCount = 0; _downloadingCount = 0;
_dtos = dtos;
} }
public AbsolutePath ModListPath(ModlistMetadata metadata) public AbsolutePath ModListPath(ModlistMetadata metadata)
@ -77,6 +81,7 @@ public class ModListDownloadMaintainer
}, path, job, token.Value); }, path, job, token.Value);
_hashCache.FileHashWriteCache(path, hash); _hashCache.FileHashWriteCache(path, hash);
await path.WithExtension(Ext.MetaData).WriteAllTextAsync(JsonSerializer.Serialize(metadata));
} }
finally finally
{ {