mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
Merge pull request #1942 from wabbajack-tools/tab-browsing-old
Tab browsing removed
This commit is contained in:
commit
84e91e28f7
@ -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>
|
@ -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;
|
||||
}
|
||||
|
49
Wabbajack.App.Wpf/Interventions/InteventionHandler.cs
Normal file
49
Wabbajack.App.Wpf/Interventions/InteventionHandler.cs
Normal 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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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()
|
||||
|
@ -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();
|
||||
|
@ -1,11 +0,0 @@
|
||||
namespace Wabbajack.Messages;
|
||||
|
||||
public class CloseBrowserTab
|
||||
{
|
||||
public BrowserTabViewModel ViewModel { get; init; }
|
||||
|
||||
public CloseBrowserTab(BrowserTabViewModel viewModel)
|
||||
{
|
||||
ViewModel = viewModel;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
namespace Wabbajack.Messages;
|
||||
|
||||
public class OpenBrowserTab
|
||||
{
|
||||
public BrowserTabViewModel ViewModel { get; set; }
|
||||
|
||||
public OpenBrowserTab(BrowserTabViewModel viewModel)
|
||||
{
|
||||
ViewModel = viewModel;
|
||||
}
|
||||
}
|
5
Wabbajack.App.Wpf/Messages/SpawnBrowserWindow.cs
Normal file
5
Wabbajack.App.Wpf/Messages/SpawnBrowserWindow.cs
Normal file
@ -0,0 +1,5 @@
|
||||
namespace Wabbajack.Messages;
|
||||
|
||||
public record SpawnBrowserWindow (BrowserWindowViewModel Vm)
|
||||
{
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
30
Wabbajack.App.Wpf/UserIntervention/ManualDownloadHandler.cs
Normal file
30
Wabbajack.App.Wpf/UserIntervention/ManualDownloadHandler.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -12,7 +12,7 @@ using Wabbajack.Services.OSIntegrated;
|
||||
|
||||
namespace Wabbajack.UserIntervention;
|
||||
|
||||
public class NexusLoginHandler : BrowserTabViewModel
|
||||
public class NexusLoginHandler : BrowserWindowViewModel
|
||||
{
|
||||
private readonly EncryptedJsonTokenProvider<NexusApiState> _tokenProvider;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
157
Wabbajack.App.Wpf/View Models/BrowserWindowViewModel.cs
Normal file
157
Wabbajack.App.Wpf/View Models/BrowserWindowViewModel.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -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 () =>
|
||||
|
@ -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;
|
||||
@ -236,48 +272,61 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM
|
||||
|
||||
private async Task BeginInstall()
|
||||
{
|
||||
InstallState = InstallState.Installing;
|
||||
var postfix = (await ModListLocation.TargetPath.ToString().Hash()).ToHex();
|
||||
await _settingsManager.Save(InstallSettingsPrefix + postfix, new SavedInstallSettings
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
ModListLocation = ModListLocation.TargetPath,
|
||||
InstallLocation = Installer.Location.TargetPath,
|
||||
DownloadLoadction = Installer.DownloadLocation.TargetPath,
|
||||
Metadata = ModlistMetadata
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
var installer = StandardInstaller.Create(_serviceProvider, new InstallerConfiguration
|
||||
InstallState = InstallState.Installing;
|
||||
var postfix = (await ModListLocation.TargetPath.ToString().Hash()).ToHex();
|
||||
await _settingsManager.Save(InstallSettingsPrefix + postfix, new SavedInstallSettings
|
||||
{
|
||||
Game = ModList.GameType,
|
||||
Downloads = Installer.DownloadLocation.TargetPath,
|
||||
Install = Installer.Location.TargetPath,
|
||||
ModList = ModList,
|
||||
ModlistArchive = ModListLocation.TargetPath,
|
||||
SystemParameters = _parametersConstructor.Create(),
|
||||
GameFolder = _gameLocator.GameLocation(ModList.GameType)
|
||||
ModListLocation = ModListLocation.TargetPath,
|
||||
InstallLocation = Installer.Location.TargetPath,
|
||||
DownloadLoadction = Installer.DownloadLocation.TargetPath,
|
||||
Metadata = ModlistMetadata
|
||||
});
|
||||
|
||||
|
||||
installer.OnStatusUpdate = update =>
|
||||
try
|
||||
{
|
||||
StatusText = update.StatusText;
|
||||
StatusProgress = update.StepsProgress;
|
||||
var installer = StandardInstaller.Create(_serviceProvider, new InstallerConfiguration
|
||||
{
|
||||
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;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TaskBarUpdate.Send($"Error during install of {ModList.Name}", TaskbarItemProgressState.Error);
|
||||
InstallState = InstallState.Failure;
|
||||
}
|
||||
TaskBarUpdate.Send(update.StatusText, TaskbarItemProgressState.Indeterminate,
|
||||
update.StepsProgress.Value);
|
||||
};
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
49
Wabbajack.App.Wpf/Views/BrowserWindow.xaml
Normal file
49
Wabbajack.App.Wpf/Views/BrowserWindow.xaml
Normal 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>
|
29
Wabbajack.App.Wpf/Views/BrowserWindow.xaml.cs
Normal file
29
Wabbajack.App.Wpf/Views/BrowserWindow.xaml.cs
Normal 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));
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
@ -212,37 +212,60 @@
|
||||
<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="OpenReadmePreInstallButton"
|
||||
Margin="30,5"
|
||||
FontSize="20"
|
||||
Style="{StaticResource LargeButtonStyle}"
|
||||
ToolTip="Open the readme for the modlist">
|
||||
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="ReadmeBrands" />
|
||||
Width="30"
|
||||
Height="30"
|
||||
VerticalAlignment="Center"
|
||||
Kind="DiscordBrands" />
|
||||
<TextBlock Grid.Column="1"
|
||||
Margin="10,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="Readme" />
|
||||
Margin="10,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="Discord" />
|
||||
</Grid>
|
||||
</Button>
|
||||
<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"
|
||||
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">
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
<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,11 +69,8 @@
|
||||
</DataTemplate>
|
||||
</ContentPresenter.Resources>
|
||||
</ContentPresenter>
|
||||
<TextBlock Grid.Row="1" Margin="5, 0" Name="ResourceUsage" HorizontalAlignment="Right"></TextBlock>
|
||||
|
||||
</Grid>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</Grid>
|
||||
|
||||
<mahapps:MetroWindow.RightWindowCommands>
|
||||
<mahapps:WindowCommands>
|
||||
@ -90,12 +83,12 @@
|
||||
Command="{Binding CopyVersionCommand}"
|
||||
Content="{Binding VersionDisplay}">
|
||||
<Button.ToolTip>
|
||||
<ToolTip Content="Wabbajack Version
Click to copy to clipboard"/>
|
||||
<ToolTip Content="Wabbajack Version
Click to copy to clipboard" />
|
||||
</Button.ToolTip>
|
||||
</Button>
|
||||
<Button Grid.Column="1"
|
||||
Margin="5,0"
|
||||
Command="{Binding OpenSettingsCommand}">
|
||||
Margin="5,0"
|
||||
Command="{Binding OpenSettingsCommand}">
|
||||
<icon:PackIconMaterial
|
||||
Width="17"
|
||||
Height="17"
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
20
Wabbajack.App.Wpf/Views/MainWindowContent.xaml
Normal file
20
Wabbajack.App.Wpf/Views/MainWindowContent.xaml
Normal 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>
|
11
Wabbajack.App.Wpf/Views/MainWindowContent.xaml.cs
Normal file
11
Wabbajack.App.Wpf/Views/MainWindowContent.xaml.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Wabbajack;
|
||||
|
||||
public partial class MainWindowContent : UserControl
|
||||
{
|
||||
public MainWindowContent()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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)
|
||||
|
@ -26,7 +26,7 @@ public class AUserIntervention<T> : IUserIntervention
|
||||
|
||||
public void Finish(T value)
|
||||
{
|
||||
_tcs.SetResult(value);
|
||||
_tcs.TrySetResult(value);
|
||||
_ct.Cancel();
|
||||
}
|
||||
|
||||
|
16
Wabbajack.DTOs/Interventions/ManualBlobDownload.cs
Normal file
16
Wabbajack.DTOs/Interventions/ManualBlobDownload.cs
Normal 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;
|
||||
}
|
||||
}
|
23
Wabbajack.DTOs/Interventions/ManualDownload.cs
Normal file
23
Wabbajack.DTOs/Interventions/ManualDownload.cs
Normal 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)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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()
|
||||
@ -104,11 +115,64 @@ public class NexusDownloader : ADownloader<Nexus>, IUrlDownloader
|
||||
public override async Task<Hash> Download(Archive archive, Nexus state, AbsolutePath destination,
|
||||
IJob job, CancellationToken token)
|
||||
{
|
||||
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);
|
||||
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,
|
||||
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)
|
||||
|
@ -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,12 +98,26 @@ 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}");
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
@ -120,7 +127,7 @@ public abstract class AInstaller<T>
|
||||
ExtractedModlistFolder = _manager.CreateFolder();
|
||||
await using var stream = _configuration.ModlistArchive.Open(FileMode.Open, FileAccess.Read, FileShare.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)
|
||||
{
|
||||
var path = entry.FullName.ToRelativePath().RelativeTo(ExtractedModlistFolder);
|
||||
@ -182,7 +189,7 @@ public abstract class AInstaller<T>
|
||||
/// </summary>
|
||||
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),
|
||||
HashedArchives);
|
||||
await _vfs.BackfillMissing();
|
||||
@ -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);
|
||||
}
|
||||
@ -280,7 +293,7 @@ public abstract class AInstaller<T>
|
||||
|
||||
_logger.LogInformation("Validating Archives");
|
||||
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);
|
||||
return;
|
||||
@ -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,47 +482,50 @@ 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
|
||||
.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 =>
|
||||
{
|
||||
// Get all the folders and all the folder parents
|
||||
// so for foo\bar\baz\qux.txt this emits ["foo", "foo\\bar", "foo\\bar\\baz"]
|
||||
var split = ((string) path.RelativeTo(_configuration.Install)).Split('\\');
|
||||
return Enumerable.Range(1, split.Length - 1).Select(t => string.Join("\\", split.Take(t)));
|
||||
})
|
||||
.ToList())
|
||||
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("_"))
|
||||
.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"]
|
||||
var split = ((string) path.RelativeTo(_configuration.Install)).Split('\\');
|
||||
return Enumerable.Range(1, split.Length - 1).Select(t => string.Join("\\", split.Take(t)));
|
||||
})
|
||||
.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)
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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>();
|
||||
|
@ -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
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user