diff --git a/Wabbajack.App.Wpf/App.xaml b/Wabbajack.App.Wpf/App.xaml
index ce244afa..fb010729 100644
--- a/Wabbajack.App.Wpf/App.xaml
+++ b/Wabbajack.App.Wpf/App.xaml
@@ -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">
-
+
+
\ No newline at end of file
diff --git a/Wabbajack.App.Wpf/App.xaml.cs b/Wabbajack.App.Wpf/App.xaml.cs
index 970d2d02..ed7f3d9a 100644
--- a/Wabbajack.App.Wpf/App.xaml.cs
+++ b/Wabbajack.App.Wpf/App.xaml.cs
@@ -77,6 +77,7 @@ namespace Wabbajack
services.AddAllSingleton();
services.AddAllSingleton();
services.AddSingleton();
+ services.AddSingleton();
return services;
}
diff --git a/Wabbajack.App.Wpf/Extensions/TabContent.cs b/Wabbajack.App.Wpf/Extensions/TabContent.cs
new file mode 100644
index 00000000..e338fcee
--- /dev/null
+++ b/Wabbajack.App.Wpf/Extensions/TabContent.cs
@@ -0,0 +1,282 @@
+// TabContent.cs, version 1.2
+// The code in this file is Copyright (c) Ivan Krivyakov
+// See http://www.ikriv.com/legal.php for more information
+//
+using System;
+using System.ComponentModel;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Markup;
+
+namespace IKriv.Windows.Controls.Behaviors
+{
+ ///
+ /// Attached properties for persistent tab control
+ ///
+ /// By default WPF TabControl bound to an ItemsSource destroys visual state of invisible tabs.
+ /// Set ikriv:TabContent.IsCached="True" to preserve visual state of each tab.
+ ///
+ public static class TabContent
+ {
+ public static bool GetIsCached(DependencyObject obj)
+ {
+ return (bool)obj.GetValue(IsCachedProperty);
+ }
+
+ public static void SetIsCached(DependencyObject obj, bool value)
+ {
+ obj.SetValue(IsCachedProperty, value);
+ }
+
+ ///
+ /// Controls whether tab content is cached or not
+ ///
+ /// When TabContent.IsCached is true, visual state of each tab is preserved (cached), even when the tab is hidden
+ public static readonly DependencyProperty IsCachedProperty =
+ DependencyProperty.RegisterAttached("IsCached", typeof(bool), typeof(TabContent), new UIPropertyMetadata(false, OnIsCachedChanged));
+
+
+ public static DataTemplate GetTemplate(DependencyObject obj)
+ {
+ return (DataTemplate)obj.GetValue(TemplateProperty);
+ }
+
+ public static void SetTemplate(DependencyObject obj, DataTemplate value)
+ {
+ obj.SetValue(TemplateProperty, value);
+ }
+
+ ///
+ /// Used instead of TabControl.ContentTemplate for cached tabs
+ ///
+ public static readonly DependencyProperty TemplateProperty =
+ DependencyProperty.RegisterAttached("Template", typeof(DataTemplate), typeof(TabContent), new UIPropertyMetadata(null));
+
+
+ public static DataTemplateSelector GetTemplateSelector(DependencyObject obj)
+ {
+ return (DataTemplateSelector)obj.GetValue(TemplateSelectorProperty);
+ }
+
+ public static void SetTemplateSelector(DependencyObject obj, DataTemplateSelector value)
+ {
+ obj.SetValue(TemplateSelectorProperty, value);
+ }
+
+ ///
+ /// Used instead of TabControl.ContentTemplateSelector for cached tabs
+ ///
+ public static readonly DependencyProperty TemplateSelectorProperty =
+ DependencyProperty.RegisterAttached("TemplateSelector", typeof(DataTemplateSelector), typeof(TabContent), new UIPropertyMetadata(null));
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static TabControl GetInternalTabControl(DependencyObject obj)
+ {
+ return (TabControl)obj.GetValue(InternalTabControlProperty);
+ }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static void SetInternalTabControl(DependencyObject obj, TabControl value)
+ {
+ obj.SetValue(InternalTabControlProperty, value);
+ }
+
+ // Using a DependencyProperty as the backing store for InternalTabControl. This enables animation, styling, binding, etc...
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static readonly DependencyProperty InternalTabControlProperty =
+ DependencyProperty.RegisterAttached("InternalTabControl", typeof(TabControl), typeof(TabContent), new UIPropertyMetadata(null, OnInternalTabControlChanged));
+
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static ContentControl GetInternalCachedContent(DependencyObject obj)
+ {
+ return (ContentControl)obj.GetValue(InternalCachedContentProperty);
+ }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static void SetInternalCachedContent(DependencyObject obj, ContentControl value)
+ {
+ obj.SetValue(InternalCachedContentProperty, value);
+ }
+
+ // Using a DependencyProperty as the backing store for InternalCachedContent. This enables animation, styling, binding, etc...
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static readonly DependencyProperty InternalCachedContentProperty =
+ DependencyProperty.RegisterAttached("InternalCachedContent", typeof(ContentControl), typeof(TabContent), new UIPropertyMetadata(null));
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static object GetInternalContentManager(DependencyObject obj)
+ {
+ return (object)obj.GetValue(InternalContentManagerProperty);
+ }
+
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public static void SetInternalContentManager(DependencyObject obj, object value)
+ {
+ obj.SetValue(InternalContentManagerProperty, value);
+ }
+
+ // Using a DependencyProperty as the backing store for InternalContentManager. This enables animation, styling, binding, etc...
+ public static readonly DependencyProperty InternalContentManagerProperty =
+ DependencyProperty.RegisterAttached("InternalContentManager", typeof(object), typeof(TabContent), new UIPropertyMetadata(null));
+
+ private static void OnIsCachedChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
+ {
+ if (obj == null) return;
+
+ var tabControl = obj as TabControl;
+ if (tabControl == null)
+ {
+ throw new InvalidOperationException("Cannot set TabContent.IsCached on object of type " + args.NewValue.GetType().Name +
+ ". Only objects of type TabControl can have TabContent.IsCached property.");
+ }
+
+ bool newValue = (bool)args.NewValue;
+
+ if (!newValue)
+ {
+ if (args.OldValue != null && ((bool)args.OldValue))
+ {
+ throw new NotImplementedException("Cannot change TabContent.IsCached from True to False. Turning tab caching off is not implemented");
+ }
+
+ return;
+ }
+
+ EnsureContentTemplateIsNull(tabControl);
+ tabControl.ContentTemplate = CreateContentTemplate();
+ EnsureContentTemplateIsNotModified(tabControl);
+ }
+
+ private static DataTemplate CreateContentTemplate()
+ {
+ const string xaml =
+ "";
+
+ var context = new ParserContext();
+
+ context.XamlTypeMapper = new XamlTypeMapper(new string[0]);
+ context.XamlTypeMapper.AddMappingProcessingInstruction("b", typeof(TabContent).Namespace, typeof(TabContent).Assembly.FullName);
+
+ context.XmlnsDictionary.Add("", "http://schemas.microsoft.com/winfx/2006/xaml/presentation");
+ context.XmlnsDictionary.Add("b", "b");
+
+ var template = (DataTemplate)XamlReader.Parse(xaml, context);
+ return template;
+ }
+
+ private static void OnInternalTabControlChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
+ {
+ if (obj == null) return;
+ var container = obj as Decorator;
+
+ if (container == null)
+ {
+ var message = "Cannot set TabContent.InternalTabControl on object of type " + obj.GetType().Name +
+ ". Only controls that derive from Decorator, such as Border can have a TabContent.InternalTabControl.";
+ throw new InvalidOperationException(message);
+ }
+
+ if (args.NewValue == null) return;
+ if (!(args.NewValue is TabControl))
+ {
+ throw new InvalidOperationException("Value of TabContent.InternalTabControl cannot be of type " + args.NewValue.GetType().Name +", it must be of type TabControl");
+ }
+
+ var tabControl = (TabControl)args.NewValue;
+ var contentManager = GetContentManager(tabControl, container);
+ contentManager.UpdateSelectedTab();
+ }
+
+ private static ContentManager GetContentManager(TabControl tabControl, Decorator container)
+ {
+ var contentManager = (ContentManager)GetInternalContentManager(tabControl);
+ if (contentManager != null)
+ {
+ /*
+ * Content manager already exists for the tab control. This means that tab content template is applied
+ * again, and new instance of the Border control (container) has been created. The old container
+ * referenced by the content manager is no longer visible and needs to be replaced
+ */
+ contentManager.ReplaceContainer(container);
+ }
+ else
+ {
+ // create content manager for the first time
+ contentManager = new ContentManager(tabControl, container);
+ SetInternalContentManager(tabControl, contentManager);
+ }
+
+ return contentManager;
+ }
+
+ private static void EnsureContentTemplateIsNull(TabControl tabControl)
+ {
+ if (tabControl.ContentTemplate != null)
+ {
+ throw new InvalidOperationException("TabControl.ContentTemplate value is not null. If TabContent.IsCached is True, use TabContent.Template instead of ContentTemplate");
+ }
+ }
+
+ private static void EnsureContentTemplateIsNotModified(TabControl tabControl)
+ {
+ var descriptor = DependencyPropertyDescriptor.FromProperty(TabControl.ContentTemplateProperty, typeof(TabControl));
+ descriptor.AddValueChanged(tabControl, (sender, args) =>
+ {
+ throw new InvalidOperationException("Cannot assign to TabControl.ContentTemplate when TabContent.IsCached is True. Use TabContent.Template instead");
+ });
+ }
+
+ public class ContentManager
+ {
+ TabControl _tabControl;
+ Decorator _border;
+
+ public ContentManager(TabControl tabControl, Decorator border)
+ {
+ _tabControl = tabControl;
+ _border = border;
+ _tabControl.SelectionChanged += (sender, args) => { UpdateSelectedTab(); };
+ }
+
+ public void ReplaceContainer(Decorator newBorder)
+ {
+ if (Object.ReferenceEquals(_border, newBorder)) return;
+
+ _border.Child = null; // detach any tab content that old border may hold
+ _border = newBorder;
+ }
+
+ public void UpdateSelectedTab()
+ {
+ _border.Child = GetCurrentContent();
+ }
+
+ private ContentControl GetCurrentContent()
+ {
+ var item = _tabControl.SelectedItem;
+ if (item == null) return null;
+
+ var tabItem = _tabControl.ItemContainerGenerator.ContainerFromItem(item);
+ if (tabItem == null) return null;
+
+ var cachedContent = TabContent.GetInternalCachedContent(tabItem);
+ if (cachedContent == null)
+ {
+ cachedContent = new ContentControl
+ {
+ DataContext = item,
+ ContentTemplate = TabContent.GetTemplate(_tabControl),
+ ContentTemplateSelector = TabContent.GetTemplateSelector(_tabControl)
+ };
+
+ cachedContent.SetBinding(ContentControl.ContentProperty, new Binding());
+ TabContent.SetInternalCachedContent(tabItem, cachedContent);
+ }
+
+ return cachedContent;
+ }
+ }
+ }
+}
diff --git a/Wabbajack.App.Wpf/Interventions/InteventionHandler.cs b/Wabbajack.App.Wpf/Interventions/InteventionHandler.cs
index d3748ac4..14a30177 100644
--- a/Wabbajack.App.Wpf/Interventions/InteventionHandler.cs
+++ b/Wabbajack.App.Wpf/Interventions/InteventionHandler.cs
@@ -14,8 +14,18 @@ public class InteventionHandler : IUserInterventionHandler
}
public void Raise(IUserIntervention intervention)
{
- // Recast these or they won't be properly handled by the message bus
- if (intervention is ManualDownload md)
- MessageBus.Current.SendMessage(md);
+ switch (intervention)
+ {
+ // Recast these or they won't be properly handled by the message bus
+ case ManualDownload md:
+ MessageBus.Current.SendMessage(md);
+ break;
+ case ManualBlobDownload bd:
+ MessageBus.Current.SendMessage(bd);
+ break;
+ default:
+ _logger.LogError("No handler for user intervention: {Type}", intervention);
+ break;
+ }
}
}
\ No newline at end of file
diff --git a/Wabbajack.App.Wpf/Models/ResourceMonitor.cs b/Wabbajack.App.Wpf/Models/ResourceMonitor.cs
index fbd006c6..97f01c7e 100644
--- a/Wabbajack.App.Wpf/Models/ResourceMonitor.cs
+++ b/Wabbajack.App.Wpf/Models/ResourceMonitor.cs
@@ -45,7 +45,6 @@ public class ResourceMonitor : IDisposable
.DisposeWith(_compositeDisposable);
_tasks.Connect()
- .Filter(t => t.IsWorking)
.Bind(out _tasksFiltered)
.Subscribe()
.DisposeWith(_compositeDisposable);
diff --git a/Wabbajack.App.Wpf/UserIntervention/ManualBlobDownloadHandler.cs b/Wabbajack.App.Wpf/UserIntervention/ManualBlobDownloadHandler.cs
new file mode 100644
index 00000000..ca46f238
--- /dev/null
+++ b/Wabbajack.App.Wpf/UserIntervention/ManualBlobDownloadHandler.cs
@@ -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 : BrowserTabViewModel
+{
+ 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);
+ }
+}
\ No newline at end of file
diff --git a/Wabbajack.App.Wpf/UserIntervention/ManualDownloadHandler.cs b/Wabbajack.App.Wpf/UserIntervention/ManualDownloadHandler.cs
index 8e3697c0..ba20bdcb 100644
--- a/Wabbajack.App.Wpf/UserIntervention/ManualDownloadHandler.cs
+++ b/Wabbajack.App.Wpf/UserIntervention/ManualDownloadHandler.cs
@@ -11,11 +11,14 @@ namespace Wabbajack.UserIntervention;
public class ManualDownloadHandler : BrowserTabViewModel
{
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);
diff --git a/Wabbajack.App.Wpf/View Models/BrowserTabViewModel.cs b/Wabbajack.App.Wpf/View Models/BrowserTabViewModel.cs
index dc770d14..810e6484 100644
--- a/Wabbajack.App.Wpf/View Models/BrowserTabViewModel.cs
+++ b/Wabbajack.App.Wpf/View Models/BrowserTabViewModel.cs
@@ -4,7 +4,6 @@ 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;
@@ -12,7 +11,9 @@ 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;
@@ -119,4 +120,31 @@ public abstract class BrowserTabViewModel : ViewModel
("Referer", referer.ToString())
});
}
+
+ public async Task 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;
+ }
}
\ No newline at end of file
diff --git a/Wabbajack.App.Wpf/View Models/MainWindowVM.cs b/Wabbajack.App.Wpf/View Models/MainWindowVM.cs
index 4b4b8f9c..9e9d5076 100644
--- a/Wabbajack.App.Wpf/View Models/MainWindowVM.cs
+++ b/Wabbajack.App.Wpf/View Models/MainWindowVM.cs
@@ -118,6 +118,11 @@ namespace Wabbajack
.Subscribe(HandleManualDownload)
.DisposeWith(CompositeDisposable);
+
+ MessageBus.Current.Listen()
+ .Subscribe(HandleManualBlobDownload)
+ .DisposeWith(CompositeDisposable);
+
_resourceMonitor.Updates
.Select(r => string.Join(", ", r.Where(r => r.Throughput > 0)
.Select(s => $"{s.Name} - {s.Throughput.ToFileSizeString()}/sec")))
@@ -197,6 +202,13 @@ namespace Wabbajack
handler.Intervention = manualDownload;
MessageBus.Current.SendMessage(new OpenBrowserTab(handler));
}
+
+ private void HandleManualBlobDownload(ManualBlobDownload manualDownload)
+ {
+ var handler = _serviceProvider.GetRequiredService();
+ handler.Intervention = manualDownload;
+ MessageBus.Current.SendMessage(new OpenBrowserTab(handler));
+ }
private void HandleNavigateTo(NavigateToGlobal.ScreenType s)
{
diff --git a/Wabbajack.App.Wpf/Views/BrowserTabView.xaml.cs b/Wabbajack.App.Wpf/Views/BrowserTabView.xaml.cs
index c81a27de..3e514008 100644
--- a/Wabbajack.App.Wpf/Views/BrowserTabView.xaml.cs
+++ b/Wabbajack.App.Wpf/Views/BrowserTabView.xaml.cs
@@ -44,8 +44,12 @@ public partial class BrowserTabView : IDisposable
private void ClickClose(object sender, RoutedEventArgs e)
{
var tc = (TabControl) this.Parent;
- if (tc.Items.Contains(this))
- tc.Items.Remove(this);
+ if (tc != null)
+ {
+ if (tc.Items.Contains(this))
+ tc.Items.Remove(this);
+ }
+
this.Dispose();
}
}
diff --git a/Wabbajack.App.Wpf/Views/MainWindow.xaml b/Wabbajack.App.Wpf/Views/MainWindow.xaml
index 1bd48b9d..fd9111e6 100644
--- a/Wabbajack.App.Wpf/Views/MainWindow.xaml
+++ b/Wabbajack.App.Wpf/Views/MainWindow.xaml
@@ -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:behaviors="clr-namespace:IKriv.Windows.Controls.Behaviors"
ShowTitleBar="False"
Title="WABBAJACK"
Width="1280"
@@ -34,7 +35,13 @@
-
+
+
+
+
+
+
+
diff --git a/Wabbajack.App.Wpf/Views/MainWindow.xaml.cs b/Wabbajack.App.Wpf/Views/MainWindow.xaml.cs
index caa1bf0c..52f5b42d 100644
--- a/Wabbajack.App.Wpf/Views/MainWindow.xaml.cs
+++ b/Wabbajack.App.Wpf/Views/MainWindow.xaml.cs
@@ -45,20 +45,20 @@ namespace Wabbajack
};
MessageBus.Current.Listen()
+ .ObserveOnGuiThread()
.Subscribe(u =>
{
- Dispatcher.Invoke(() =>
- {
- TaskbarItemInfo.Description = u.Description;
- TaskbarItemInfo.ProgressValue = u.ProgressValue;
- TaskbarItemInfo.ProgressState = u.State;
- });
+ TaskbarItemInfo.Description = u.Description;
+ TaskbarItemInfo.ProgressValue = u.ProgressValue;
+ TaskbarItemInfo.ProgressState = u.State;
});
MessageBus.Current.Listen()
+ .ObserveOnGuiThread()
.Subscribe(OnOpenBrowserTab);
MessageBus.Current.Listen()
+ .ObserveOnGuiThread()
.Subscribe(OnCloseBrowserTab);
_logger.LogInformation("Wabbajack Build - {Sha}",ThisAssembly.Git.Sha);
@@ -107,9 +107,10 @@ namespace Wabbajack
{
this.Topmost = false;
};
-
+/*
((MainWindowVM) DataContext).WhenAnyValue(vm => vm.OpenSettingsCommand)
.BindTo(this, view => view.SettingsButton.Command);
+ */
}
catch (Exception ex)
{
@@ -117,13 +118,13 @@ 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);
+ .BindToStrict(this, view => view.ResourceUsage.Visibility);*/
}
diff --git a/Wabbajack.App.Wpf/Views/MainWindowContent.xaml b/Wabbajack.App.Wpf/Views/MainWindowContent.xaml
new file mode 100644
index 00000000..6f545965
--- /dev/null
+++ b/Wabbajack.App.Wpf/Views/MainWindowContent.xaml
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/Wabbajack.App.Wpf/Views/MainWindowContent.xaml.cs b/Wabbajack.App.Wpf/Views/MainWindowContent.xaml.cs
new file mode 100644
index 00000000..28fd9c06
--- /dev/null
+++ b/Wabbajack.App.Wpf/Views/MainWindowContent.xaml.cs
@@ -0,0 +1,11 @@
+using System.Windows.Controls;
+
+namespace Wabbajack.App.Wpf.Views;
+
+public partial class MainWindowContent : UserControl
+{
+ public MainWindowContent()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/Wabbajack.Common/HttpExtensions.cs b/Wabbajack.Common/HttpExtensions.cs
index bcfaaa0c..a0900120 100644
--- a/Wabbajack.Common/HttpExtensions.cs
+++ b/Wabbajack.Common/HttpExtensions.cs
@@ -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;
@@ -17,6 +19,15 @@ public static class HttpExtensions
msg.Headers.Add("Cookie", string.Join(";", cookies.Select(c => $"{c.Name}={c.Value}")));
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)
{
@@ -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 GetFromJsonAsync(this HttpClient client, IResource limiter,
HttpRequestMessage msg,
JsonSerializerOptions? options, CancellationToken cancellationToken = default)
diff --git a/Wabbajack.DTOs/Interventions/AUserIntervention.cs b/Wabbajack.DTOs/Interventions/AUserIntervention.cs
index e9a5a2b0..9897f150 100644
--- a/Wabbajack.DTOs/Interventions/AUserIntervention.cs
+++ b/Wabbajack.DTOs/Interventions/AUserIntervention.cs
@@ -26,7 +26,7 @@ public class AUserIntervention : IUserIntervention
public void Finish(T value)
{
- _tcs.SetResult(value);
+ _tcs.TrySetResult(value);
_ct.Cancel();
}
diff --git a/Wabbajack.DTOs/Interventions/ManualBlobDownload.cs b/Wabbajack.DTOs/Interventions/ManualBlobDownload.cs
new file mode 100644
index 00000000..061b49af
--- /dev/null
+++ b/Wabbajack.DTOs/Interventions/ManualBlobDownload.cs
@@ -0,0 +1,16 @@
+using Wabbajack.Hashing.xxHash64;
+using Wabbajack.Paths;
+
+namespace Wabbajack.DTOs.Interventions;
+
+public class ManualBlobDownload : AUserIntervention
+{
+ public Archive Archive { get; }
+ public AbsolutePath Destination { get; }
+
+ public ManualBlobDownload(Archive archive, AbsolutePath destination)
+ {
+ Archive = archive;
+ Destination = destination;
+ }
+}
\ No newline at end of file
diff --git a/Wabbajack.DTOs/Interventions/ManualDownload.cs b/Wabbajack.DTOs/Interventions/ManualDownload.cs
index d9d16a83..15a3be24 100644
--- a/Wabbajack.DTOs/Interventions/ManualDownload.cs
+++ b/Wabbajack.DTOs/Interventions/ManualDownload.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Threading;
using Wabbajack.DTOs.Logins;
using Wabbajack.Hashing.xxHash64;
using Wabbajack.Paths;
@@ -9,12 +10,10 @@ namespace Wabbajack.DTOs.Interventions;
public class ManualDownload : AUserIntervention
{
public Archive Archive { get; }
- public AbsolutePath OutputPath { get; }
- public ManualDownload(Archive archive, AbsolutePath outputPath)
+ public ManualDownload(Archive archive)
{
Archive = archive;
- OutputPath = outputPath;
}
public record BrowserDownloadState(Uri Uri, Cookie[] Cookies, (string Key, string Value)[] Headers)
diff --git a/Wabbajack.Downloaders.Manual/ManualDownloader.cs b/Wabbajack.Downloaders.Manual/ManualDownloader.cs
index 172b48a6..14d99ecb 100644
--- a/Wabbajack.Downloaders.Manual/ManualDownloader.cs
+++ b/Wabbajack.Downloaders.Manual/ManualDownloader.cs
@@ -1,4 +1,5 @@
using Microsoft.Extensions.Logging;
+using Wabbajack.Common;
using Wabbajack.Downloaders.Interfaces;
using Wabbajack.DTOs;
using Wabbajack.DTOs.DownloadStates;
@@ -28,28 +29,33 @@ public class ManualDownloader : ADownloader
public override async Task Download(Archive archive, DTOs.DownloadStates.Manual state, AbsolutePath destination, IJob job, CancellationToken token)
{
_logger.LogInformation("Starting manual download of {Url}", state.Url);
- var intervention = new ManualDownload(archive, destination);
- _interventionHandler.Raise(intervention);
- var browserState = await intervention.Task;
- var msg = new HttpRequestMessage(HttpMethod.Get, browserState.Uri);
- msg.Headers.Add("User-Agent",
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36");
- msg.Headers.Add("Cookie", string.Join(";", browserState.Cookies.Select(c => $"{c.Name}={c.Value}")));
-
-
- foreach (var header in browserState.Headers)
+ if (state.Url.Host == "mega.nz")
{
- msg.Headers.Add(header.Key, header.Value);
+ 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;
- using var response = await _client.SendAsync(msg, token);
- if (!response.IsSuccessStatusCode)
- throw new HttpRequestException(response.ReasonPhrase, null, statusCode:response.StatusCode);
+ var msg = browserState.ToHttpRequestMessage();
- 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);
+ 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);
+ }
}
diff --git a/Wabbajack.Downloaders.Manual/Wabbajack.Downloaders.Manual.csproj b/Wabbajack.Downloaders.Manual/Wabbajack.Downloaders.Manual.csproj
index 30490c37..17b55750 100644
--- a/Wabbajack.Downloaders.Manual/Wabbajack.Downloaders.Manual.csproj
+++ b/Wabbajack.Downloaders.Manual/Wabbajack.Downloaders.Manual.csproj
@@ -7,6 +7,7 @@
+
diff --git a/Wabbajack.Downloaders.Nexus/NexusDownloader.cs b/Wabbajack.Downloaders.Nexus/NexusDownloader.cs
index 985f3912..4899c8b1 100644
--- a/Wabbajack.Downloaders.Nexus/NexusDownloader.cs
+++ b/Wabbajack.Downloaders.Nexus/NexusDownloader.cs
@@ -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, IUrlDownloader
private readonly HttpClient _client;
private readonly IHttpDownloader _downloader;
private readonly ILogger _logger;
+ private readonly IUserInterventionHandler _userInterventionHandler;
+ private readonly IResource _interventionLimiter;
+
+ private const bool IsManualDebugMode = true;
public NexusDownloader(ILogger logger, HttpClient client, IHttpDownloader downloader,
- NexusApi api)
+ NexusApi api, IUserInterventionHandler userInterventionHandler, IResource interventionLimiter)
{
_logger = logger;
_client = client;
_downloader = downloader;
_api = api;
+ _userInterventionHandler = userInterventionHandler;
+ _interventionLimiter = interventionLimiter;
}
public override async Task Prepare()
@@ -104,11 +115,59 @@ public class NexusDownloader : ADownloader, IUrlDownloader
public override async Task 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))
+ {
+ using var _ = await _interventionLimiter.Begin("Downloading file manually", 1, 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 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}")
+ }
+ });
+ _userInterventionHandler.Raise(md);
+ var browserState = await md.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 Verify(Archive archive, Nexus state, IJob job, CancellationToken token)
diff --git a/Wabbajack.Installer/AInstaller.cs b/Wabbajack.Installer/AInstaller.cs
index 717becfe..406878b6 100644
--- a/Wabbajack.Installer/AInstaller.cs
+++ b/Wabbajack.Installer/AInstaller.cs
@@ -115,7 +115,7 @@ public abstract class AInstaller
{
Interlocked.Add(ref _currentStepProgress, stepProgress);
- OnStatusUpdate?.Invoke(new StatusUpdate(_statusCategory, _statusText,
+ OnStatusUpdate?.Invoke(new StatusUpdate(_statusCategory, $"[{_currentStep}/{MaxSteps}] {_statusText} ({_currentStepProgress}/{MaxStepProgress})",
Percent.FactoryPutInRange(_currentStep, MaxSteps),
Percent.FactoryPutInRange(_currentStepProgress, MaxStepProgress)));
}
@@ -305,6 +305,9 @@ public abstract class AInstaller
public async Task DownloadMissingArchives(List 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);
@@ -312,12 +315,10 @@ public abstract class AInstaller
{
var outputPath = _configuration.Downloads.Combine(a.Name);
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)
diff --git a/Wabbajack.Networking.NexusApi/NexusApi.cs b/Wabbajack.Networking.NexusApi/NexusApi.cs
index be2da24a..6daadfc7 100644
--- a/Wabbajack.Networking.NexusApi/NexusApi.cs
+++ b/Wabbajack.Networking.NexusApi/NexusApi.cs
@@ -30,6 +30,8 @@ public class NexusApi
private readonly IResource _limiter;
private readonly ILogger _logger;
protected readonly ITokenProvider ApiKey;
+ private DateTime _lastValidated;
+ private (ValidateInfo info, ResponseMetadata header) _lastValidatedInfo;
public NexusApi(ITokenProvider apiKey, ILogger logger, HttpClient client,
IResource 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(
@@ -49,6 +53,19 @@ public class NexusApi
var msg = await GenerateMessage(HttpMethod.Get, Endpoints.Validate);
return await Send(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(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 IsPremium(CancellationToken token)
+ {
+ var validated = await ValidateCached(token);
+ return validated.info.IsPremium;
+ }
}
\ No newline at end of file
diff --git a/Wabbajack.Services.OSIntegrated/ServiceExtensions.cs b/Wabbajack.Services.OSIntegrated/ServiceExtensions.cs
index 2124e29d..836f6ca9 100644
--- a/Wabbajack.Services.OSIntegrated/ServiceExtensions.cs
+++ b/Wabbajack.Services.OSIntegrated/ServiceExtensions.cs
@@ -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;
@@ -99,6 +100,9 @@ public static class ServiceExtensions
service.AddAllSingleton>(s =>
new Resource("Installer", GetSettings(s, "Installer")));
+
+ service.AddAllSingleton>(s =>
+ new Resource("User Intervention", 3));
service.AddSingleton();