mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
Wip, trying to fix a bug with cached tabs
This commit is contained in:
parent
f93f52b1c9
commit
5907fc6fcb
@ -3,17 +3,20 @@
|
|||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:local="clr-namespace:Wabbajack"
|
xmlns:local="clr-namespace:Wabbajack"
|
||||||
|
xmlns:controls="clr-namespace:Wabbajack.Extensions"
|
||||||
ShutdownMode="OnExplicitShutdown"
|
ShutdownMode="OnExplicitShutdown"
|
||||||
Startup="OnStartup"
|
Startup="OnStartup"
|
||||||
Exit="OnExit">
|
Exit="OnExit">
|
||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
<ResourceDictionary.MergedDictionaries>
|
<ResourceDictionary.MergedDictionaries>
|
||||||
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/Dark.Purple.xaml" />
|
<ResourceDictionary
|
||||||
|
Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/Dark.Purple.xaml" />
|
||||||
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
|
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
|
||||||
<ResourceDictionary Source="Themes\Styles.xaml" />
|
<ResourceDictionary Source="Themes\Styles.xaml" />
|
||||||
<ResourceDictionary Source="Themes\CustomControls.xaml" />
|
<ResourceDictionary Source="Themes\CustomControls.xaml" />
|
||||||
</ResourceDictionary.MergedDictionaries>
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
</Application.Resources>
|
</Application.Resources>
|
||||||
</Application>
|
</Application>
|
@ -77,6 +77,7 @@ namespace Wabbajack
|
|||||||
services.AddAllSingleton<INeedsLogin, NexusLoginManager>();
|
services.AddAllSingleton<INeedsLogin, NexusLoginManager>();
|
||||||
services.AddAllSingleton<INeedsLogin, VectorPlexusLoginManager>();
|
services.AddAllSingleton<INeedsLogin, VectorPlexusLoginManager>();
|
||||||
services.AddSingleton<ManualDownloadHandler>();
|
services.AddSingleton<ManualDownloadHandler>();
|
||||||
|
services.AddSingleton<ManualBlobDownloadHandler>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
282
Wabbajack.App.Wpf/Extensions/TabContent.cs
Normal file
282
Wabbajack.App.Wpf/Extensions/TabContent.cs
Normal file
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Attached properties for persistent tab control
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>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.
|
||||||
|
/// </remarks>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controls whether tab content is cached or not
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>When TabContent.IsCached is true, visual state of each tab is preserved (cached), even when the tab is hidden</remarks>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used instead of TabControl.ContentTemplate for cached tabs
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used instead of TabControl.ContentTemplateSelector for cached tabs
|
||||||
|
/// </summary>
|
||||||
|
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 =
|
||||||
|
"<DataTemplate><Border b:TabContent.InternalTabControl=\"{Binding RelativeSource={RelativeSource AncestorType=TabControl}}\" /></DataTemplate>";
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -14,8 +14,18 @@ public class InteventionHandler : IUserInterventionHandler
|
|||||||
}
|
}
|
||||||
public void Raise(IUserIntervention intervention)
|
public void Raise(IUserIntervention intervention)
|
||||||
{
|
{
|
||||||
// Recast these or they won't be properly handled by the message bus
|
switch (intervention)
|
||||||
if (intervention is ManualDownload md)
|
{
|
||||||
MessageBus.Current.SendMessage(md);
|
// 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -45,7 +45,6 @@ public class ResourceMonitor : IDisposable
|
|||||||
.DisposeWith(_compositeDisposable);
|
.DisposeWith(_compositeDisposable);
|
||||||
|
|
||||||
_tasks.Connect()
|
_tasks.Connect()
|
||||||
.Filter(t => t.IsWorking)
|
|
||||||
.Bind(out _tasksFiltered)
|
.Bind(out _tasksFiltered)
|
||||||
.Subscribe()
|
.Subscribe()
|
||||||
.DisposeWith(_compositeDisposable);
|
.DisposeWith(_compositeDisposable);
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -11,12 +11,15 @@ namespace Wabbajack.UserIntervention;
|
|||||||
public class ManualDownloadHandler : BrowserTabViewModel
|
public class ManualDownloadHandler : BrowserTabViewModel
|
||||||
{
|
{
|
||||||
public ManualDownload Intervention { get; set; }
|
public ManualDownload Intervention { get; set; }
|
||||||
|
|
||||||
protected override async Task Run(CancellationToken token)
|
protected override async Task Run(CancellationToken token)
|
||||||
{
|
{
|
||||||
await WaitForReady();
|
await WaitForReady();
|
||||||
var archive = Intervention.Archive;
|
var archive = Intervention.Archive;
|
||||||
var md = Intervention.Archive.State as Manual;
|
var md = Intervention.Archive.State as Manual;
|
||||||
|
|
||||||
|
HeaderText = $"Manual download ({md.Url.Host})";
|
||||||
|
|
||||||
Instructions = string.IsNullOrWhiteSpace(md.Prompt) ? $"Please download {archive.Name}" : md.Prompt;
|
Instructions = string.IsNullOrWhiteSpace(md.Prompt) ? $"Please download {archive.Name}" : md.Prompt;
|
||||||
await NavigateTo(md.Url);
|
await NavigateTo(md.Url);
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@ using System.Linq;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Web;
|
|
||||||
using HtmlAgilityPack;
|
using HtmlAgilityPack;
|
||||||
using Microsoft.Web.WebView2.Core;
|
using Microsoft.Web.WebView2.Core;
|
||||||
using Microsoft.Web.WebView2.Wpf;
|
using Microsoft.Web.WebView2.Wpf;
|
||||||
@ -12,7 +11,9 @@ using ReactiveUI;
|
|||||||
using ReactiveUI.Fody.Helpers;
|
using ReactiveUI.Fody.Helpers;
|
||||||
using Wabbajack.DTOs.Interventions;
|
using Wabbajack.DTOs.Interventions;
|
||||||
using Wabbajack.DTOs.Logins;
|
using Wabbajack.DTOs.Logins;
|
||||||
|
using Wabbajack.Hashing.xxHash64;
|
||||||
using Wabbajack.Messages;
|
using Wabbajack.Messages;
|
||||||
|
using Wabbajack.Paths;
|
||||||
using Wabbajack.Views;
|
using Wabbajack.Views;
|
||||||
|
|
||||||
namespace Wabbajack;
|
namespace Wabbajack;
|
||||||
@ -119,4 +120,31 @@ public abstract class BrowserTabViewModel : ViewModel
|
|||||||
("Referer", referer.ToString())
|
("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;
|
||||||
|
}
|
||||||
}
|
}
|
@ -118,6 +118,11 @@ namespace Wabbajack
|
|||||||
.Subscribe(HandleManualDownload)
|
.Subscribe(HandleManualDownload)
|
||||||
.DisposeWith(CompositeDisposable);
|
.DisposeWith(CompositeDisposable);
|
||||||
|
|
||||||
|
|
||||||
|
MessageBus.Current.Listen<ManualBlobDownload>()
|
||||||
|
.Subscribe(HandleManualBlobDownload)
|
||||||
|
.DisposeWith(CompositeDisposable);
|
||||||
|
|
||||||
_resourceMonitor.Updates
|
_resourceMonitor.Updates
|
||||||
.Select(r => string.Join(", ", r.Where(r => r.Throughput > 0)
|
.Select(r => string.Join(", ", r.Where(r => r.Throughput > 0)
|
||||||
.Select(s => $"{s.Name} - {s.Throughput.ToFileSizeString()}/sec")))
|
.Select(s => $"{s.Name} - {s.Throughput.ToFileSizeString()}/sec")))
|
||||||
@ -198,6 +203,13 @@ namespace Wabbajack
|
|||||||
MessageBus.Current.SendMessage(new OpenBrowserTab(handler));
|
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 HandleNavigateTo(NavigateToGlobal.ScreenType s)
|
private void HandleNavigateTo(NavigateToGlobal.ScreenType s)
|
||||||
{
|
{
|
||||||
if (s is NavigateToGlobal.ScreenType.Settings)
|
if (s is NavigateToGlobal.ScreenType.Settings)
|
||||||
|
@ -44,8 +44,12 @@ public partial class BrowserTabView : IDisposable
|
|||||||
private void ClickClose(object sender, RoutedEventArgs e)
|
private void ClickClose(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var tc = (TabControl) this.Parent;
|
var tc = (TabControl) this.Parent;
|
||||||
if (tc.Items.Contains(this))
|
if (tc != null)
|
||||||
tc.Items.Remove(this);
|
{
|
||||||
|
if (tc.Items.Contains(this))
|
||||||
|
tc.Items.Remove(this);
|
||||||
|
}
|
||||||
|
|
||||||
this.Dispose();
|
this.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
xmlns:mahapps="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
|
xmlns:mahapps="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:viewModels="clr-namespace:Wabbajack.View_Models"
|
xmlns:viewModels="clr-namespace:Wabbajack.View_Models"
|
||||||
|
xmlns:behaviors="clr-namespace:IKriv.Windows.Controls.Behaviors"
|
||||||
ShowTitleBar="False"
|
ShowTitleBar="False"
|
||||||
Title="WABBAJACK"
|
Title="WABBAJACK"
|
||||||
Width="1280"
|
Width="1280"
|
||||||
@ -34,7 +35,13 @@
|
|||||||
</LinearGradientBrush>
|
</LinearGradientBrush>
|
||||||
</Rectangle.Fill>
|
</Rectangle.Fill>
|
||||||
</Rectangle>
|
</Rectangle>
|
||||||
<TabControl Grid.Row="0" x:Name="Tabs">
|
<TabControl Grid.Row="0" x:Name="Tabs" behaviors:TabContent.IsCached="True">
|
||||||
|
<behaviors:TabContent.Template>
|
||||||
|
<DataTemplate>
|
||||||
|
<Button>Sup></Button>
|
||||||
|
</DataTemplate>
|
||||||
|
</behaviors:TabContent.Template>
|
||||||
|
<!--
|
||||||
<TabItem>
|
<TabItem>
|
||||||
<TabItem.Header>
|
<TabItem.Header>
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
@ -42,7 +49,6 @@
|
|||||||
<Button Name="SettingsButton"><icon:Material Kind="Cog"></icon:Material></Button>
|
<Button Name="SettingsButton"><icon:Material Kind="Cog"></icon:Material></Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</TabItem.Header>
|
</TabItem.Header>
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="*"></RowDefinition>
|
<RowDefinition Height="*"></RowDefinition>
|
||||||
@ -75,7 +81,7 @@
|
|||||||
</ContentPresenter>
|
</ContentPresenter>
|
||||||
<TextBlock Grid.Row="1" Margin="5, 0" Name="ResourceUsage" HorizontalAlignment="Right"></TextBlock>
|
<TextBlock Grid.Row="1" Margin="5, 0" Name="ResourceUsage" HorizontalAlignment="Right"></TextBlock>
|
||||||
</Grid>
|
</Grid>
|
||||||
</TabItem>
|
</TabItem> -->
|
||||||
</TabControl>
|
</TabControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
@ -45,20 +45,20 @@ namespace Wabbajack
|
|||||||
};
|
};
|
||||||
|
|
||||||
MessageBus.Current.Listen<TaskBarUpdate>()
|
MessageBus.Current.Listen<TaskBarUpdate>()
|
||||||
|
.ObserveOnGuiThread()
|
||||||
.Subscribe(u =>
|
.Subscribe(u =>
|
||||||
{
|
{
|
||||||
Dispatcher.Invoke(() =>
|
TaskbarItemInfo.Description = u.Description;
|
||||||
{
|
TaskbarItemInfo.ProgressValue = u.ProgressValue;
|
||||||
TaskbarItemInfo.Description = u.Description;
|
TaskbarItemInfo.ProgressState = u.State;
|
||||||
TaskbarItemInfo.ProgressValue = u.ProgressValue;
|
|
||||||
TaskbarItemInfo.ProgressState = u.State;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
MessageBus.Current.Listen<OpenBrowserTab>()
|
MessageBus.Current.Listen<OpenBrowserTab>()
|
||||||
|
.ObserveOnGuiThread()
|
||||||
.Subscribe(OnOpenBrowserTab);
|
.Subscribe(OnOpenBrowserTab);
|
||||||
|
|
||||||
MessageBus.Current.Listen<CloseBrowserTab>()
|
MessageBus.Current.Listen<CloseBrowserTab>()
|
||||||
|
.ObserveOnGuiThread()
|
||||||
.Subscribe(OnCloseBrowserTab);
|
.Subscribe(OnCloseBrowserTab);
|
||||||
|
|
||||||
_logger.LogInformation("Wabbajack Build - {Sha}",ThisAssembly.Git.Sha);
|
_logger.LogInformation("Wabbajack Build - {Sha}",ThisAssembly.Git.Sha);
|
||||||
@ -107,9 +107,10 @@ namespace Wabbajack
|
|||||||
{
|
{
|
||||||
this.Topmost = false;
|
this.Topmost = false;
|
||||||
};
|
};
|
||||||
|
/*
|
||||||
((MainWindowVM) DataContext).WhenAnyValue(vm => vm.OpenSettingsCommand)
|
((MainWindowVM) DataContext).WhenAnyValue(vm => vm.OpenSettingsCommand)
|
||||||
.BindTo(this, view => view.SettingsButton.Command);
|
.BindTo(this, view => view.SettingsButton.Command);
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -117,13 +118,13 @@ namespace Wabbajack
|
|||||||
Environment.Exit(-1);
|
Environment.Exit(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
vm.WhenAnyValue(vm => vm.ResourceStatus)
|
vm.WhenAnyValue(vm => vm.ResourceStatus)
|
||||||
.BindToStrict(this, view => view.ResourceUsage.Text);
|
.BindToStrict(this, view => view.ResourceUsage.Text);
|
||||||
|
|
||||||
vm.WhenAnyValue(vm => vm.ResourceStatus)
|
vm.WhenAnyValue(vm => vm.ResourceStatus)
|
||||||
.Select(x => string.IsNullOrWhiteSpace(x) ? Visibility.Collapsed : Visibility.Visible)
|
.Select(x => string.IsNullOrWhiteSpace(x) ? Visibility.Collapsed : Visibility.Visible)
|
||||||
.BindToStrict(this, view => view.ResourceUsage.Visibility);
|
.BindToStrict(this, view => view.ResourceUsage.Visibility);*/
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
12
Wabbajack.App.Wpf/Views/MainWindowContent.xaml
Normal file
12
Wabbajack.App.Wpf/Views/MainWindowContent.xaml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<UserControl x:Class="Wabbajack.App.Wpf.Views.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.App.Wpf.Views"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="300" d:DesignWidth="300">
|
||||||
|
<Grid>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</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.App.Wpf.Views;
|
||||||
|
|
||||||
|
public partial class MainWindowContent : UserControl
|
||||||
|
{
|
||||||
|
public MainWindowContent()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,11 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Wabbajack.DTOs.Interventions;
|
||||||
using Wabbajack.DTOs.Logins;
|
using Wabbajack.DTOs.Logins;
|
||||||
using Wabbajack.Networking.Http;
|
using Wabbajack.Networking.Http;
|
||||||
using Wabbajack.RateLimiter;
|
using Wabbajack.RateLimiter;
|
||||||
@ -18,6 +20,15 @@ public static class HttpExtensions
|
|||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static HttpRequestMessage AddHeaders(this HttpRequestMessage msg, IEnumerable<(string Key, string Value)> headers)
|
||||||
|
{
|
||||||
|
foreach (var header in headers)
|
||||||
|
{
|
||||||
|
msg.Headers.Add(header.Key, header.Value);
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
public static HttpRequestMessage AddChromeAgent(this HttpRequestMessage msg)
|
public static HttpRequestMessage AddChromeAgent(this HttpRequestMessage msg)
|
||||||
{
|
{
|
||||||
msg.Headers.Add("User-Agent",
|
msg.Headers.Add("User-Agent",
|
||||||
@ -25,6 +36,15 @@ public static class HttpExtensions
|
|||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static HttpRequestMessage ToHttpRequestMessage(this ManualDownload.BrowserDownloadState browserState)
|
||||||
|
{
|
||||||
|
var msg = new HttpRequestMessage(HttpMethod.Get, browserState.Uri);
|
||||||
|
msg.AddChromeAgent();
|
||||||
|
msg.AddCookies(browserState.Cookies);
|
||||||
|
msg.AddHeaders(browserState.Headers);
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
public static async Task<TValue?> GetFromJsonAsync<TValue>(this HttpClient client, IResource<HttpClient> limiter,
|
public static async Task<TValue?> GetFromJsonAsync<TValue>(this HttpClient client, IResource<HttpClient> limiter,
|
||||||
HttpRequestMessage msg,
|
HttpRequestMessage msg,
|
||||||
JsonSerializerOptions? options, CancellationToken cancellationToken = default)
|
JsonSerializerOptions? options, CancellationToken cancellationToken = default)
|
||||||
|
@ -26,7 +26,7 @@ public class AUserIntervention<T> : IUserIntervention
|
|||||||
|
|
||||||
public void Finish(T value)
|
public void Finish(T value)
|
||||||
{
|
{
|
||||||
_tcs.SetResult(value);
|
_tcs.TrySetResult(value);
|
||||||
_ct.Cancel();
|
_ct.Cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
using Wabbajack.DTOs.Logins;
|
using Wabbajack.DTOs.Logins;
|
||||||
using Wabbajack.Hashing.xxHash64;
|
using Wabbajack.Hashing.xxHash64;
|
||||||
using Wabbajack.Paths;
|
using Wabbajack.Paths;
|
||||||
@ -9,12 +10,10 @@ namespace Wabbajack.DTOs.Interventions;
|
|||||||
public class ManualDownload : AUserIntervention<ManualDownload.BrowserDownloadState>
|
public class ManualDownload : AUserIntervention<ManualDownload.BrowserDownloadState>
|
||||||
{
|
{
|
||||||
public Archive Archive { get; }
|
public Archive Archive { get; }
|
||||||
public AbsolutePath OutputPath { get; }
|
|
||||||
|
|
||||||
public ManualDownload(Archive archive, AbsolutePath outputPath)
|
public ManualDownload(Archive archive)
|
||||||
{
|
{
|
||||||
Archive = archive;
|
Archive = archive;
|
||||||
OutputPath = outputPath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public record BrowserDownloadState(Uri Uri, Cookie[] Cookies, (string Key, string Value)[] Headers)
|
public record BrowserDownloadState(Uri Uri, Cookie[] Cookies, (string Key, string Value)[] Headers)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Wabbajack.Common;
|
||||||
using Wabbajack.Downloaders.Interfaces;
|
using Wabbajack.Downloaders.Interfaces;
|
||||||
using Wabbajack.DTOs;
|
using Wabbajack.DTOs;
|
||||||
using Wabbajack.DTOs.DownloadStates;
|
using Wabbajack.DTOs.DownloadStates;
|
||||||
@ -28,28 +29,33 @@ public class ManualDownloader : ADownloader<DTOs.DownloadStates.Manual>
|
|||||||
public override async Task<Hash> Download(Archive archive, DTOs.DownloadStates.Manual state, AbsolutePath destination, IJob job, CancellationToken token)
|
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);
|
_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);
|
if (state.Url.Host == "mega.nz")
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
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);
|
var msg = browserState.ToHttpRequestMessage();
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
throw new HttpRequestException(response.ReasonPhrase, null, statusCode:response.StatusCode);
|
|
||||||
|
|
||||||
await using var strm = await response.Content.ReadAsStreamAsync(token);
|
using var response = await _client.SendAsync(msg, token);
|
||||||
await using var os = destination.Open(FileMode.Create, FileAccess.Write, FileShare.None);
|
if (!response.IsSuccessStatusCode)
|
||||||
return await strm.HashingCopy(os, token, job);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Wabbajack.Common\Wabbajack.Common.csproj" />
|
||||||
<ProjectReference Include="..\Wabbajack.Downloaders.Interfaces\Wabbajack.Downloaders.Interfaces.csproj" />
|
<ProjectReference Include="..\Wabbajack.Downloaders.Interfaces\Wabbajack.Downloaders.Interfaces.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
@ -1,20 +1,25 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Web;
|
using System.Web;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Wabbajack.Common;
|
||||||
using Wabbajack.Downloaders.Interfaces;
|
using Wabbajack.Downloaders.Interfaces;
|
||||||
using Wabbajack.DTOs;
|
using Wabbajack.DTOs;
|
||||||
using Wabbajack.DTOs.DownloadStates;
|
using Wabbajack.DTOs.DownloadStates;
|
||||||
|
using Wabbajack.DTOs.Interventions;
|
||||||
using Wabbajack.DTOs.Validation;
|
using Wabbajack.DTOs.Validation;
|
||||||
using Wabbajack.Hashing.xxHash64;
|
using Wabbajack.Hashing.xxHash64;
|
||||||
using Wabbajack.Networking.Http;
|
using Wabbajack.Networking.Http;
|
||||||
using Wabbajack.Networking.Http.Interfaces;
|
using Wabbajack.Networking.Http.Interfaces;
|
||||||
using Wabbajack.Networking.NexusApi;
|
using Wabbajack.Networking.NexusApi;
|
||||||
using Wabbajack.Paths;
|
using Wabbajack.Paths;
|
||||||
|
using Wabbajack.Paths.IO;
|
||||||
using Wabbajack.RateLimiter;
|
using Wabbajack.RateLimiter;
|
||||||
|
|
||||||
namespace Wabbajack.Downloaders;
|
namespace Wabbajack.Downloaders;
|
||||||
@ -25,14 +30,20 @@ public class NexusDownloader : ADownloader<Nexus>, IUrlDownloader
|
|||||||
private readonly HttpClient _client;
|
private readonly HttpClient _client;
|
||||||
private readonly IHttpDownloader _downloader;
|
private readonly IHttpDownloader _downloader;
|
||||||
private readonly ILogger<NexusDownloader> _logger;
|
private readonly ILogger<NexusDownloader> _logger;
|
||||||
|
private readonly IUserInterventionHandler _userInterventionHandler;
|
||||||
|
private readonly IResource<IUserInterventionHandler> _interventionLimiter;
|
||||||
|
|
||||||
|
private const bool IsManualDebugMode = true;
|
||||||
|
|
||||||
public NexusDownloader(ILogger<NexusDownloader> logger, HttpClient client, IHttpDownloader downloader,
|
public NexusDownloader(ILogger<NexusDownloader> logger, HttpClient client, IHttpDownloader downloader,
|
||||||
NexusApi api)
|
NexusApi api, IUserInterventionHandler userInterventionHandler, IResource<IUserInterventionHandler> interventionLimiter)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_client = client;
|
_client = client;
|
||||||
_downloader = downloader;
|
_downloader = downloader;
|
||||||
_api = api;
|
_api = api;
|
||||||
|
_userInterventionHandler = userInterventionHandler;
|
||||||
|
_interventionLimiter = interventionLimiter;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<bool> Prepare()
|
public override async Task<bool> Prepare()
|
||||||
@ -104,11 +115,59 @@ public class NexusDownloader : ADownloader<Nexus>, IUrlDownloader
|
|||||||
public override async Task<Hash> Download(Archive archive, Nexus state, AbsolutePath destination,
|
public override async Task<Hash> Download(Archive archive, Nexus state, AbsolutePath destination,
|
||||||
IJob job, CancellationToken token)
|
IJob job, CancellationToken token)
|
||||||
{
|
{
|
||||||
var urls = await _api.DownloadLink(state.Game.MetaData().NexusName!, state.ModID, state.FileID, token);
|
if (IsManualDebugMode || await _api.IsPremium(token))
|
||||||
_logger.LogInformation("Downloading Nexus File: {game}|{modid}|{fileid}", state.Game, state.ModID,
|
{
|
||||||
state.FileID);
|
using var _ = await _interventionLimiter.Begin("Downloading file manually", 1, token);
|
||||||
var message = new HttpRequestMessage(HttpMethod.Get, urls.info.First().URI);
|
return await DownloadManually(archive, state, destination, job, token);
|
||||||
return await _downloader.Download(message, destination, job, token);
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var urls = await _api.DownloadLink(state.Game.MetaData().NexusName!, state.ModID, state.FileID, token);
|
||||||
|
_logger.LogInformation("Downloading Nexus File: {game}|{modid}|{fileid}", state.Game, state.ModID,
|
||||||
|
state.FileID);
|
||||||
|
var message = new HttpRequestMessage(HttpMethod.Get, urls.info.First().URI);
|
||||||
|
return await _downloader.Download(message, destination, job, token);
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
if (ex.StatusCode == HttpStatusCode.Forbidden)
|
||||||
|
{
|
||||||
|
return await DownloadManually(archive, state, destination, job, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Hash> DownloadManually(Archive archive, Nexus state, AbsolutePath destination, IJob job, CancellationToken token)
|
||||||
|
{
|
||||||
|
var md = new ManualDownload(new Archive
|
||||||
|
{
|
||||||
|
Name = archive.Name,
|
||||||
|
Hash = archive.Hash,
|
||||||
|
Meta = archive.Meta,
|
||||||
|
Size = archive.Size,
|
||||||
|
State = new Manual
|
||||||
|
{
|
||||||
|
Prompt = "Click Download - Buy Nexus Premium to automate this process",
|
||||||
|
Url = new Uri($"https://www.nexusmods.com/{state.Game.MetaData().NexusName}/mods/{state.ModID}?tab=files&file_id={state.FileID}")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_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<bool> Verify(Archive archive, Nexus state, IJob job, CancellationToken token)
|
public override async Task<bool> Verify(Archive archive, Nexus state, IJob job, CancellationToken token)
|
||||||
|
@ -115,7 +115,7 @@ public abstract class AInstaller<T>
|
|||||||
{
|
{
|
||||||
Interlocked.Add(ref _currentStepProgress, stepProgress);
|
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(_currentStep, MaxSteps),
|
||||||
Percent.FactoryPutInRange(_currentStepProgress, MaxStepProgress)));
|
Percent.FactoryPutInRange(_currentStepProgress, MaxStepProgress)));
|
||||||
}
|
}
|
||||||
@ -305,6 +305,9 @@ public abstract class AInstaller<T>
|
|||||||
|
|
||||||
public async Task DownloadMissingArchives(List<Archive> missing, CancellationToken token, bool download = true)
|
public async Task DownloadMissingArchives(List<Archive> missing, CancellationToken token, bool download = true)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation("Downloading {Count} archives", missing.Count.ToString());
|
||||||
|
NextStep(Consts.StepDownloading, "Downloading files", missing.Count);
|
||||||
|
|
||||||
if (download)
|
if (download)
|
||||||
{
|
{
|
||||||
var result = SendDownloadMetrics(missing);
|
var result = SendDownloadMetrics(missing);
|
||||||
@ -312,12 +315,10 @@ public abstract class AInstaller<T>
|
|||||||
{
|
{
|
||||||
var outputPath = _configuration.Downloads.Combine(a.Name);
|
var outputPath = _configuration.Downloads.Combine(a.Name);
|
||||||
await DownloadArchive(a, true, token, outputPath);
|
await DownloadArchive(a, true, token, outputPath);
|
||||||
|
UpdateProgress(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Downloading {Count} archives", missing.Count.ToString());
|
|
||||||
NextStep(Consts.StepDownloading, "Downloading files", missing.Count);
|
|
||||||
|
|
||||||
await missing
|
await missing
|
||||||
.OrderBy(a => a.Size)
|
.OrderBy(a => a.Size)
|
||||||
.Where(a => a.State is not Manual)
|
.Where(a => a.State is not Manual)
|
||||||
|
@ -30,6 +30,8 @@ public class NexusApi
|
|||||||
private readonly IResource<HttpClient> _limiter;
|
private readonly IResource<HttpClient> _limiter;
|
||||||
private readonly ILogger<NexusApi> _logger;
|
private readonly ILogger<NexusApi> _logger;
|
||||||
protected readonly ITokenProvider<NexusApiState> ApiKey;
|
protected readonly ITokenProvider<NexusApiState> ApiKey;
|
||||||
|
private DateTime _lastValidated;
|
||||||
|
private (ValidateInfo info, ResponseMetadata header) _lastValidatedInfo;
|
||||||
|
|
||||||
public NexusApi(ITokenProvider<NexusApiState> apiKey, ILogger<NexusApi> logger, HttpClient client,
|
public NexusApi(ITokenProvider<NexusApiState> apiKey, ILogger<NexusApi> logger, HttpClient client,
|
||||||
IResource<HttpClient> limiter,
|
IResource<HttpClient> limiter,
|
||||||
@ -41,6 +43,8 @@ public class NexusApi
|
|||||||
_appInfo = appInfo;
|
_appInfo = appInfo;
|
||||||
_jsonOptions = jsonOptions;
|
_jsonOptions = jsonOptions;
|
||||||
_limiter = limiter;
|
_limiter = limiter;
|
||||||
|
_lastValidated = DateTime.MinValue;
|
||||||
|
_lastValidatedInfo = default;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual async Task<(ValidateInfo info, ResponseMetadata header)> Validate(
|
public virtual async Task<(ValidateInfo info, ResponseMetadata header)> Validate(
|
||||||
@ -50,6 +54,19 @@ public class NexusApi
|
|||||||
return await Send<ValidateInfo>(msg, token);
|
return await Send<ValidateInfo>(msg, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<(ValidateInfo info, ResponseMetadata header)> ValidateCached(
|
||||||
|
CancellationToken token = default)
|
||||||
|
{
|
||||||
|
if (DateTime.Now - _lastValidated < TimeSpan.FromMinutes(10))
|
||||||
|
{
|
||||||
|
return _lastValidatedInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg = await GenerateMessage(HttpMethod.Get, Endpoints.Validate);
|
||||||
|
_lastValidatedInfo = await Send<ValidateInfo>(msg, token);
|
||||||
|
return _lastValidatedInfo;
|
||||||
|
}
|
||||||
|
|
||||||
public virtual async Task<(ModInfo info, ResponseMetadata header)> ModInfo(string nexusGameName, long modId,
|
public virtual async Task<(ModInfo info, ResponseMetadata header)> ModInfo(string nexusGameName, long modId,
|
||||||
CancellationToken token = default)
|
CancellationToken token = default)
|
||||||
{
|
{
|
||||||
@ -310,4 +327,10 @@ public class NexusApi
|
|||||||
await Task.Delay(TimeSpan.FromSeconds(5));
|
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> IsPremium(CancellationToken token)
|
||||||
|
{
|
||||||
|
var validated = await ValidateCached(token);
|
||||||
|
return validated.info.IsPremium;
|
||||||
|
}
|
||||||
}
|
}
|
@ -9,6 +9,7 @@ using Wabbajack.Compiler;
|
|||||||
using Wabbajack.Downloaders;
|
using Wabbajack.Downloaders;
|
||||||
using Wabbajack.Downloaders.GameFile;
|
using Wabbajack.Downloaders.GameFile;
|
||||||
using Wabbajack.DTOs;
|
using Wabbajack.DTOs;
|
||||||
|
using Wabbajack.DTOs.Interventions;
|
||||||
using Wabbajack.DTOs.Logins;
|
using Wabbajack.DTOs.Logins;
|
||||||
using Wabbajack.Installer;
|
using Wabbajack.Installer;
|
||||||
using Wabbajack.Networking.BethesdaNet;
|
using Wabbajack.Networking.BethesdaNet;
|
||||||
@ -100,6 +101,9 @@ public static class ServiceExtensions
|
|||||||
service.AddAllSingleton<IResource, IResource<IInstaller>>(s =>
|
service.AddAllSingleton<IResource, IResource<IInstaller>>(s =>
|
||||||
new Resource<IInstaller>("Installer", GetSettings(s, "Installer")));
|
new Resource<IInstaller>("Installer", GetSettings(s, "Installer")));
|
||||||
|
|
||||||
|
service.AddAllSingleton<IResource, IResource<IUserInterventionHandler>>(s =>
|
||||||
|
new Resource<IUserInterventionHandler>("User Intervention", 3));
|
||||||
|
|
||||||
service.AddSingleton<LoggingRateLimiterReporter>();
|
service.AddSingleton<LoggingRateLimiterReporter>();
|
||||||
|
|
||||||
service.AddScoped<Context>();
|
service.AddScoped<Context>();
|
||||||
|
Loading…
Reference in New Issue
Block a user