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
commit 84e91e28f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Wabbajack"
xmlns:controls="clr-namespace:Wabbajack.Extensions"
ShutdownMode="OnExplicitShutdown"
Startup="OnStartup"
Exit="OnExit">
<Application.Resources>
<ResourceDictionary>
<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="Themes\Styles.xaml" />
<ResourceDictionary Source="Themes\CustomControls.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@ -8,6 +8,8 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ReactiveUI;
using Wabbajack.DTOs;
using Wabbajack.DTOs.Interventions;
using Wabbajack.Interventions;
using Wabbajack.LoginManagers;
using Wabbajack.Models;
using Wabbajack.Services.OSIntegrated;
@ -46,9 +48,11 @@ namespace Wabbajack
services.AddOSIntegrated();
services.AddSingleton<CefService>();
services.AddSingleton<IUserInterventionHandler, UserInteventionHandler>();
services.AddTransient<MainWindow>();
services.AddTransient<MainWindowVM>();
services.AddTransient<BrowserWindow>();
services.AddSingleton<SystemParametersConstructor>();
services.AddSingleton<LauncherUpdater>();
services.AddSingleton<ResourceMonitor>();
@ -73,6 +77,8 @@ namespace Wabbajack
services.AddAllSingleton<INeedsLogin, LoversLabLoginManager>();
services.AddAllSingleton<INeedsLogin, NexusLoginManager>();
services.AddAllSingleton<INeedsLogin, VectorPlexusLoginManager>();
services.AddSingleton<ManualDownloadHandler>();
services.AddSingleton<ManualBlobDownloadHandler>();
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.Media;
using System.Windows.Media.Imaging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
@ -15,6 +16,7 @@ using Wabbajack.DTOs.Interventions;
using Wabbajack.DTOs.Logins;
using Wabbajack.Messages;
using Wabbajack.Networking.Http.Interfaces;
using Wabbajack.UserIntervention;
namespace Wabbajack.LoginManagers;
@ -23,6 +25,7 @@ public class LoversLabLoginManager : ViewModel, INeedsLogin
private readonly ILogger<LoversLabLoginManager> _logger;
private readonly ITokenProvider<LoversLabLoginState> _token;
private readonly IUserInterventionHandler _handler;
private readonly IServiceProvider _serviceProvider;
public string SiteName { get; } = "Lovers Lab";
public ICommand TriggerLogin { get; set; }
@ -33,10 +36,11 @@ public class LoversLabLoginManager : ViewModel, INeedsLogin
[Reactive]
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;
_token = token;
_serviceProvider = serviceProvider;
RefreshTokenState();
ClearLogin = ReactiveCommand.CreateFromTask(async () =>
@ -52,11 +56,19 @@ public class LoversLabLoginManager : ViewModel, INeedsLogin
TriggerLogin = ReactiveCommand.CreateFromTask(async () =>
{
_logger.LogInformation("Logging into {SiteName}", SiteName);
await LoversLabLogin.Send();
RefreshTokenState();
StartLogin();
}, 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()
{
HaveLogin = _token.HaveToken();

View File

@ -52,12 +52,18 @@ public class NexusLoginManager : ViewModel, INeedsLogin
TriggerLogin = ReactiveCommand.CreateFromTask(async () =>
{
_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));
}
MessageBus.Current.Listen<CloseBrowserTab>()
.Subscribe(x => RefreshTokenState())
.DisposeWith(CompositeDisposable);
private void StartLogin()
{
var view = new BrowserWindow();
view.Closed += (sender, args) => { RefreshTokenState(); };
var provider = _serviceProvider.GetRequiredService<NexusLoginHandler>();
view.DataContext = provider;
view.Show();
}
private void RefreshTokenState()

View File

@ -7,6 +7,7 @@ using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
@ -15,6 +16,7 @@ using Wabbajack.DTOs.Interventions;
using Wabbajack.DTOs.Logins;
using Wabbajack.Messages;
using Wabbajack.Networking.Http.Interfaces;
using Wabbajack.UserIntervention;
namespace Wabbajack.LoginManagers;
@ -23,6 +25,7 @@ public class VectorPlexusLoginManager : ViewModel, INeedsLogin
private readonly ILogger<VectorPlexusLoginManager> _logger;
private readonly ITokenProvider<VectorPlexusLoginState> _token;
private readonly IUserInterventionHandler _handler;
private readonly IServiceProvider _serviceProvider;
public string SiteName { get; } = "Vector Plexus";
public ICommand TriggerLogin { get; set; }
@ -33,10 +36,11 @@ public class VectorPlexusLoginManager : ViewModel, INeedsLogin
[Reactive]
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;
_token = token;
_serviceProvider = serviceProvider;
RefreshTokenState();
ClearLogin = ReactiveCommand.CreateFromTask(async () =>
@ -52,11 +56,21 @@ public class VectorPlexusLoginManager : ViewModel, INeedsLogin
TriggerLogin = ReactiveCommand.CreateFromTask(async () =>
{
_logger.LogInformation("Logging into {SiteName}", SiteName);
await VectorPlexusLogin.Send();
RefreshTokenState();
StartLogin();
}, 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()
{
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 DynamicData;
using DynamicData.Kernel;
using Microsoft.Extensions.Logging;
using ReactiveUI;
using Wabbajack.RateLimiter;
@ -14,7 +15,7 @@ namespace Wabbajack.Models;
public class ResourceMonitor : IDisposable
{
private readonly TimeSpan PollInterval = TimeSpan.FromMilliseconds(1000);
private readonly TimeSpan PollInterval = TimeSpan.FromMilliseconds(250);
private readonly IResource[] _resources;
private readonly Timer _timer;
@ -27,13 +28,15 @@ public class ResourceMonitor : IDisposable
private readonly SourceCache<CPUDisplayVM, ulong> _tasks = new(x => x.ID);
public readonly ReadOnlyObservableCollection<CPUDisplayVM> _tasksFiltered;
private readonly CompositeDisposable _compositeDisposable;
private readonly ILogger<ResourceMonitor> _logger;
public ReadOnlyObservableCollection<CPUDisplayVM> Tasks => _tasksFiltered;
public ResourceMonitor(IEnumerable<IResource> resources)
public ResourceMonitor(ILogger<ResourceMonitor> logger, IEnumerable<IResource> resources)
{
_logger = logger;
_compositeDisposable = new CompositeDisposable();
_resources = resources.ToArray();
_prev = _resources.Select(x => (x.Name, (long)0)).ToArray();
@ -71,6 +74,7 @@ public class ResourceMonitor : IDisposable
var t = tsk.Value;
t.Msg = job.Description;
t.ProgressPercent = job.Size == 0 ? Percent.Zero : Percent.FactoryPutInRange(job.Current, (long)job.Size);
t.IsWorking = job.Current > 0;
}
// Create
@ -81,7 +85,8 @@ public class ResourceMonitor : IDisposable
ID = job.ID,
StartTime = DateTime.Now,
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);
}

View File

@ -8,7 +8,7 @@ using Wabbajack.Services.OSIntegrated;
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)
: 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;
public class NexusLoginHandler : BrowserTabViewModel
public class NexusLoginHandler : BrowserWindowViewModel
{
private readonly EncryptedJsonTokenProvider<NexusApiState> _tokenProvider;

View File

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

View File

@ -7,7 +7,7 @@ using Wabbajack.Services.OSIntegrated;
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)
: 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 () =>
{
_parent.MWVM.ModListContentsVM.Value.Name = metadata.Title;
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);
}
UIUtils.OpenWebsite(new Uri("https://www.wabbajack.org/search/" + Metadata.NamespacedName));
}, IsLoadingIdle.StartWith(true));
ExecuteCommand = ReactiveCommand.CreateFromTask(async () =>

View File

@ -6,6 +6,8 @@ using System.Windows.Media.Imaging;
using ReactiveUI.Fody.Helpers;
using DynamicData;
using System.Reactive;
using System.Reactive.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Shell;
@ -112,6 +114,7 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM
// Command properties
public ReactiveCommand<Unit, Unit> ShowManifestCommand { get; }
public ReactiveCommand<Unit, Unit> OpenReadmeCommand { get; }
public ReactiveCommand<Unit, Unit> OpenDiscordButton { get; }
public ReactiveCommand<Unit, Unit> VisitModListWebsiteCommand { get; }
public ReactiveCommand<Unit, Unit> CloseWhenCompleteCommand { get; }
@ -164,6 +167,25 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM
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(() =>
{
UIUtils.OpenFolder(Installer.Location.TargetPath);
@ -214,6 +236,20 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM
var hex = (await ModListLocation.TargetPath.ToString().Hash()).ToHex();
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)
{
ModListLocation.TargetPath = prevSettings.ModListLocation;
@ -235,6 +271,8 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM
}
private async Task BeginInstall()
{
await Task.Run(async () =>
{
InstallState = InstallState.Installing;
var postfix = (await ModListLocation.TargetPath.ToString().Hash()).ToHex();
@ -265,19 +303,30 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM
StatusText = update.StatusText;
StatusProgress = update.StepsProgress;
TaskBarUpdate.Send(update.StatusText, TaskbarItemProgressState.Indeterminate, update.StepsProgress.Value);
TaskBarUpdate.Send(update.StatusText, TaskbarItemProgressState.Indeterminate,
update.StepsProgress.Value);
};
await installer.Begin(CancellationToken.None);
if (!await installer.Begin(CancellationToken.None))
{
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.Interventions;
using Wabbajack.Interventions;
using Wabbajack.Paths;
using Wabbajack.Util;
namespace Wabbajack
@ -53,6 +54,15 @@ namespace Wabbajack
PathType = FilePickerVM.PathTypeOptions.Folder,
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()
{
ExistCheckOption = FilePickerVM.CheckOptions.Off,

View File

@ -17,6 +17,7 @@ using Microsoft.Extensions.Logging;
using Wabbajack.Common;
using Wabbajack.Downloaders.GameFile;
using Wabbajack;
using Wabbajack.DTOs.Interventions;
using Wabbajack.Interventions;
using Wabbajack.LoginManagers;
using Wabbajack.Messages;
@ -67,6 +68,9 @@ namespace Wabbajack
[Reactive]
public string ResourceStatus { get; set; }
[Reactive]
public string AppName { get; set; }
[Reactive]
public bool UpdateAvailable { get; private set; }
@ -101,16 +105,9 @@ namespace Wabbajack
.Subscribe(HandleNavigateBack)
.DisposeWith(CompositeDisposable);
MessageBus.Current.Listen<NexusLogin>()
.Subscribe(HandleLogin)
.DisposeWith(CompositeDisposable);
MessageBus.Current.Listen<LoversLabLogin>()
.Subscribe(HandleLogin)
.DisposeWith(CompositeDisposable);
MessageBus.Current.Listen<VectorPlexusLogin>()
.Subscribe(HandleLogin)
MessageBus.Current.Listen<SpawnBrowserWindow>()
.ObserveOnGuiThread()
.Subscribe(HandleSpawnBrowserWindow)
.DisposeWith(CompositeDisposable);
_resourceMonitor.Updates
@ -136,6 +133,7 @@ namespace Wabbajack
var fvi = FileVersionInfo.GetVersionInfo(assembly.Location);
Consts.CurrentMinimumWabbajackVersion = Version.Parse(fvi.FileVersion);
VersionDisplay = $"v{fvi.FileVersion}";
AppName = "WABBAJACK " + VersionDisplay;
_logger.LogInformation("Wabbajack Version: {FileVersion}", fvi.FileVersion);
Task.Run(() => _wjClient.SendMetric("started_wabbajack", fvi.FileVersion)).FireAndForget();
@ -162,31 +160,33 @@ namespace Wabbajack
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)
{
ActivePane = PreviousPanes.Last();
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)
{
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"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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="*" />
</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"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"

View File

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

View File

@ -31,7 +31,7 @@ namespace Wabbajack
.DisposeWith(disposables);
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)
.DisposeWith(disposables);
@ -43,10 +43,22 @@ namespace Wabbajack
.BindToStrict(this, view => view.OpenReadmePreInstallButton.Command)
.DisposeWith(disposables);
ViewModel.WhenAnyValue(vm => vm.OpenDiscordButton)
.BindToStrict(this, view => view.OpenDiscordPreInstallButton.Command)
.DisposeWith(disposables);
ViewModel.WhenAnyValue(vm => vm.VisitModListWebsiteCommand)
.BindToStrict(this, view => view.OpenWebsite.Command)
.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)
.Select(loading => loading ? Visibility.Visible : Visibility.Collapsed)
.BindToStrict(this, view => view.ModlistLoadingRing.Visibility)

View File

@ -8,6 +8,7 @@
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:views="clr-namespace:Wabbajack.Views"
ShowTitleBar="False"
Title="WABBAJACK"
Width="1280"
@ -22,33 +23,28 @@
UseLayoutRounding="True"
WindowTitleBrush="{StaticResource MahApps.Brushes.Accent}"
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.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</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>
<DataTemplate DataType="{x:Type local:CompilerVM}">
<local:CompilerView ViewModel="{Binding}" />
@ -73,10 +69,7 @@
</DataTemplate>
</ContentPresenter.Resources>
</ContentPresenter>
<TextBlock Grid.Row="1" Margin="5, 0" Name="ResourceUsage" HorizontalAlignment="Right"></TextBlock>
</Grid>
</TabItem>
</TabControl>
</Grid>
<mahapps:MetroWindow.RightWindowCommands>

View File

@ -1,16 +1,24 @@
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using DynamicData;
using DynamicData.Binding;
using MahApps.Metro.Controls;
using Microsoft.Extensions.Logging;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.Common;
using Wabbajack.DTOs;
using Wabbajack.DTOs.DownloadStates;
using Wabbajack.DTOs.Interventions;
using Wabbajack.Messages;
using Wabbajack.Paths.IO;
using Wabbajack.UserIntervention;
using Wabbajack.Util;
using Wabbajack.Views;
@ -26,12 +34,13 @@ namespace Wabbajack
private readonly ILogger<MainWindow> _logger;
private readonly SystemParametersConstructor _systemParams;
private ObservableCollection<ViewModel> TabVMs = new ObservableCollectionExtended<ViewModel>();
public MainWindow(ILogger<MainWindow> logger, SystemParametersConstructor systemParams, LauncherUpdater updater, MainWindowVM vm)
{
InitializeComponent();
_mwvm = vm;
DataContext = _mwvm;
DataContext = vm;
_logger = logger;
_systemParams = systemParams;
try
@ -45,6 +54,7 @@ namespace Wabbajack
};
MessageBus.Current.Listen<TaskBarUpdate>()
.ObserveOnGuiThread()
.Subscribe(u =>
{
TaskbarItemInfo.Description = u.Description;
@ -52,11 +62,6 @@ namespace Wabbajack
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("Running in {EntryPoint}", KnownFolders.EntryPoint);
@ -107,6 +112,7 @@ namespace Wabbajack
((MainWindowVM) DataContext).WhenAnyValue(vm => vm.OpenSettingsCommand)
.BindTo(this, view => view.SettingsButton.Command);
}
catch (Exception ex)
{
@ -114,13 +120,10 @@ namespace Wabbajack
Environment.Exit(-1);
}
vm.WhenAnyValue(vm => vm.ResourceStatus)
.BindToStrict(this, view => view.ResourceUsage.Text);
vm.WhenAnyValue(vm => vm.ResourceStatus)
.Select(x => string.IsNullOrWhiteSpace(x) ? Visibility.Collapsed : Visibility.Visible)
.BindToStrict(this, view => view.ResourceUsage.Visibility);
vm.WhenAnyValue(vm => vm.AppName)
.BindToStrict(this, view => view.AppName.Text);
}
@ -200,21 +203,5 @@ namespace Wabbajack
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)
.DisposeWith(disposables);
ViewModel.WhenAnyValue(x => x.ModListContentsCommend)
.BindToStrict(this, x => x.ModListContentsButton.Command)
.DisposeWith(disposables);
ViewModel.WhenAnyValue(x => x.ExecuteCommand)
.BindToStrict(this, x => x.ExecuteButton.Command)
.DisposeWith(disposables);

View File

@ -1,9 +1,11 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Wabbajack.DTOs.Interventions;
using Wabbajack.DTOs.Logins;
using Wabbajack.Networking.Http;
using Wabbajack.RateLimiter;
@ -18,6 +20,15 @@ public static class HttpExtensions
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)
{
msg.Headers.Add("User-Agent",
@ -25,6 +36,15 @@ public static class HttpExtensions
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,
HttpRequestMessage msg,
JsonSerializerOptions? options, CancellationToken cancellationToken = default)

View File

@ -26,7 +26,7 @@ public class AUserIntervention<T> : IUserIntervention
public void Finish(T value)
{
_tcs.SetResult(value);
_tcs.TrySetResult(value);
_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.DownloadStates;
using Wabbajack.DTOs.Interventions;
using Wabbajack.DTOs.Validation;
using Wabbajack.Hashing.xxHash64;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
using Wabbajack.RateLimiter;
namespace Wabbajack.Downloaders.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()
{
return true;

View File

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

View File

@ -1,20 +1,25 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Microsoft.Extensions.Logging;
using Wabbajack.Common;
using Wabbajack.Downloaders.Interfaces;
using Wabbajack.DTOs;
using Wabbajack.DTOs.DownloadStates;
using Wabbajack.DTOs.Interventions;
using Wabbajack.DTOs.Validation;
using Wabbajack.Hashing.xxHash64;
using Wabbajack.Networking.Http;
using Wabbajack.Networking.Http.Interfaces;
using Wabbajack.Networking.NexusApi;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
using Wabbajack.RateLimiter;
namespace Wabbajack.Downloaders;
@ -25,14 +30,20 @@ public class NexusDownloader : ADownloader<Nexus>, IUrlDownloader
private readonly HttpClient _client;
private readonly IHttpDownloader _downloader;
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,
NexusApi api)
NexusApi api, IUserInterventionHandler userInterventionHandler, IResource<IUserInterventionHandler> interventionLimiter)
{
_logger = logger;
_client = client;
_downloader = downloader;
_api = api;
_userInterventionHandler = userInterventionHandler;
_interventionLimiter = interventionLimiter;
}
public override async Task<bool> Prepare()
@ -103,6 +114,14 @@ public class NexusDownloader : ADownloader<Nexus>, IUrlDownloader
public override async Task<Hash> Download(Archive archive, Nexus state, AbsolutePath destination,
IJob job, CancellationToken token)
{
if (IsManualDebugMode || await _api.IsPremium(token))
{
return await DownloadManually(archive, state, 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,
@ -110,6 +129,51 @@ public class NexusDownloader : ADownloader<Nexus>, IUrlDownloader
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)
{

View File

@ -6,6 +6,7 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Wabbajack.Common;
using Wabbajack.Downloaders.Interfaces;
using Wabbajack.DTOs;
using Wabbajack.DTOs.CDN;
@ -33,12 +34,14 @@ public class WabbajackCDNDownloader : ADownloader<WabbajackCDN>, IUrlDownloader,
private readonly DTOSerializer _dtos;
private readonly ILogger<WabbajackCDNDownloader> _logger;
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;
_logger = logger;
_dtos = dtos;
_limiter = limiter;
}
public override Task<bool> Prepare()
@ -78,8 +81,11 @@ public class WabbajackCDNDownloader : ADownloader<WabbajackCDN>, IUrlDownloader,
var definition = (await GetDefinition(state, token))!;
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}"));
using var response = await _client.SendAsync(msg, HttpCompletionOption.ResponseHeadersRead, token);
if (!response.IsSuccessStatusCode)
@ -92,13 +98,27 @@ public class WabbajackCDNDownloader : ADownloader<WabbajackCDN>, IUrlDownloader,
await using var data = await response.Content.ReadAsStreamAsync(token);
fs.Position = part.Offset;
var hash = await data.HashingCopy(fs, token, job);
var ms = new MemoryStream();
var hash = await data.HashingCopy(ms, token, partJob);
ms.Position = 0;
if (hash != part.Hash)
throw new InvalidDataException($"Bad part hash, got {hash} expected {part.Hash} for part {part.Index}");
await fs.FlushAsync(token);
{
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);
});
return definition.Hash;
}

View File

@ -321,7 +321,7 @@ public class FileExtractor
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 lastPercent = 0;

View File

@ -39,12 +39,13 @@ public abstract class AInstaller<T>
where T : AInstaller<T>
{
private const int _limitMS = 100;
public static RelativePath BSACreationDir = "TEMP_BSA_FILES".ToRelativePath();
private static readonly Regex NoDeleteRegex = new(@"(?i)[\\\/]\[NoDelete\]", RegexOptions.Compiled);
protected readonly InstallerConfiguration _configuration;
protected readonly DownloadDispatcher _downloadDispatcher;
private readonly FileExtractor.FileExtractor _extractor;
private readonly FileHashCache _fileHashCache;
protected readonly FileHashCache FileHashCache;
protected readonly IGameLocator _gameLocator;
private readonly DTOSerializer _jsonSerializer;
protected readonly ILogger<T> _logger;
@ -62,14 +63,18 @@ public abstract class AInstaller<T>
private readonly Stopwatch _updateStopWatch = new();
public Action<StatusUpdate>? OnStatusUpdate;
private readonly IResource<IInstaller> _limiter;
public AInstaller(ILogger<T> logger, InstallerConfiguration config, IGameLocator gameLocator,
FileExtractor.FileExtractor extractor,
DTOSerializer jsonSerializer, Context vfs, FileHashCache fileHashCache,
DownloadDispatcher downloadDispatcher,
ParallelOptions parallelOptions, Client wjClient)
ParallelOptions parallelOptions,
IResource<IInstaller> limiter,
Client wjClient)
{
_limiter = limiter;
_manager = new TemporaryFileManager(config.Install.Combine("__temp__"));
ExtractedModlistFolder = _manager.CreateFolder();
_configuration = config;
@ -77,7 +82,7 @@ public abstract class AInstaller<T>
_extractor = extractor;
_jsonSerializer = jsonSerializer;
_vfs = vfs;
_fileHashCache = fileHashCache;
FileHashCache = fileHashCache;
_downloadDispatcher = downloadDispatcher;
_parallelOptions = parallelOptions;
_gameLocator = gameLocator;
@ -110,7 +115,9 @@ public abstract class AInstaller<T>
{
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);
@ -210,9 +217,13 @@ public abstract class AInstaller<T>
if (grouped.Count == 0) return;
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;
UpdateProgress(file.Size);
@ -260,6 +271,8 @@ public abstract class AInstaller<T>
default:
throw new Exception($"No handler for {directive}");
}
await job.Report((int) directive.VF.Size, token);
}
}, token);
}
@ -292,19 +305,20 @@ public abstract class AInstaller<T>
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)
{
var result = SendDownloadMetrics(missing);
foreach (var a in missing.Where(a => a.State is Manual))
{
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
.OrderBy(a => a.Size)
.Where(a => a.State is not Manual)
@ -323,7 +337,7 @@ public abstract class AInstaller<T>
outputPath.Delete();
}
await DownloadArchive(archive, download, token, outputPath);
var hash = await DownloadArchive(archive, download, token, outputPath);
UpdateProgress(1);
});
}
@ -346,8 +360,20 @@ public abstract class AInstaller<T>
var (result, hash) =
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)
_fileHashCache.FileHashWriteCache(destination.Value, hash);
FileHashCache.FileHashWriteCache(destination.Value, hash);
if (result == DownloadResult.Update)
await destination.Value.MoveToAsync(destination.Value.Parent.Combine(archive.Hash.ToHex()), true,
@ -386,7 +412,7 @@ public abstract class AInstaller<T>
.PMapAll(async e =>
{
UpdateProgress(1);
return (await _fileHashCache.FileHashCachedAsync(e, token), e);
return (await FileHashCache.FileHashCachedAsync(e, token), e);
})
.ToList();
@ -407,19 +433,46 @@ public abstract class AInstaller<T>
{
_logger.LogInformation("Optimizing ModList directives");
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 savePath = (RelativePath) "saves";
var existingFiles = _configuration.Install.EnumerateFiles().ToList();
NextStep(Consts.StepPreparing, "Looking for files to delete", existingFiles.Count);
await existingFiles
NextStep(Consts.StepPreparing, "Looking for files to delete", 0);
await _configuration.Install.EnumerateFiles()
.PDoAll(async f =>
{
UpdateProgress(1);
var relativeTo = f.RelativeTo(_configuration.Install);
if (indexed.ContainsKey(relativeTo) || f.InFolder(_configuration.Downloads))
return;
@ -429,17 +482,18 @@ public abstract class AInstaller<T>
if (NoDeleteRegex.IsMatch(f.ToString()))
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();
});
_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))
// We ignore the last part of the path, so we need a dummy file name
.Append(_configuration.Downloads.Combine("_"))
.OnEach(_ => UpdateProgress(1))
.Where(f => f.InFolder(_configuration.Install))
.SelectMany(path =>
{
@ -448,28 +502,30 @@ public abstract class AInstaller<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()
.Select(p => _configuration.Install.Combine(p))
.ToHashSet();
try
{
var toDelete = _configuration.Install.EnumerateDirectories()
var toDelete = _configuration.Install.EnumerateDirectories(true)
.Where(p => !expectedFolders.Contains(p))
.OrderByDescending(p => p.ToString().Length)
.ToList();
foreach (var dir in toDelete) dir.DeleteDirectory(true);
foreach (var dir in toDelete)
{
dir.DeleteDirectory(dontDeleteIfNotEmpty: true);
}
}
catch (Exception)
{
// 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();
NextStep(Consts.StepPreparing, "Removing redundant directives", indexed.Count);
NextStep(Consts.StepPreparing, "Looking for unmodified files", 0);
await indexed.Values.PMapAll<Directive, Directive?>(async d =>
{
// 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);
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 =>
{
UpdateProgress(1);
if (d != null) indexed.Remove(d.To);
if (d != null)
{
indexed.Remove(d.To);
}
});
_logger.LogInformation("Optimized {optimized} directives to {indexed} required", ModList.Directives.Length,
indexed.Count);
NextStep(Consts.StepPreparing, "Finalizing modlist optimization", 0);
NextStep(Consts.StepPreparing, "Updating ModList", 0);
_logger.LogInformation("Optimized {From} directives to {To} required", ModList.Directives.Length, indexed.Count);
var requiredArchives = indexed.Values.OfType<FromArchive>()
.GroupBy(d => d.ArchiveHashPath.Hash)
.Select(d => d.Key)

View File

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

View File

@ -30,6 +30,8 @@ public class NexusApi
private readonly IResource<HttpClient> _limiter;
private readonly ILogger<NexusApi> _logger;
protected readonly ITokenProvider<NexusApiState> ApiKey;
private DateTime _lastValidated;
private (ValidateInfo info, ResponseMetadata header) _lastValidatedInfo;
public NexusApi(ITokenProvider<NexusApiState> apiKey, ILogger<NexusApi> logger, HttpClient client,
IResource<HttpClient> limiter,
@ -41,6 +43,8 @@ public class NexusApi
_appInfo = appInfo;
_jsonOptions = jsonOptions;
_limiter = limiter;
_lastValidated = DateTime.MinValue;
_lastValidatedInfo = default;
}
public virtual async Task<(ValidateInfo info, ResponseMetadata header)> Validate(
@ -50,6 +54,19 @@ public class NexusApi
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,
CancellationToken token = default)
{
@ -310,4 +327,10 @@ public class NexusApi
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);
}
public bool StartsWith(string s)
{
return ToString().StartsWith(s);
}
}

View File

@ -9,6 +9,7 @@ using Wabbajack.Compiler;
using Wabbajack.Downloaders;
using Wabbajack.Downloaders.GameFile;
using Wabbajack.DTOs;
using Wabbajack.DTOs.Interventions;
using Wabbajack.DTOs.Logins;
using Wabbajack.Installer;
using Wabbajack.Networking.BethesdaNet;
@ -97,6 +98,12 @@ public static class ServiceExtensions
service.AddAllSingleton<IResource, IResource<ACompiler>>(s =>
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.AddScoped<Context>();

View File

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