Merge pull request #1849 from wabbajack-tools/blazor-with-browser

Blazor with browser, log into the nexus
This commit is contained in:
Timothy Baldridge 2022-01-30 08:22:27 -07:00 committed by GitHub
commit a551fce80b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 433 additions and 11 deletions

View File

@ -4,5 +4,12 @@
Startup="OnStartup"
Exit="OnExit">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/Dark.Purple.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@ -10,6 +10,7 @@ using Wabbajack.App.Blazor.Utility;
using Wabbajack.Services.OSIntegrated;
using Blazored.Modal;
using Blazored.Toast;
using Wabbajack.App.Blazor.Browser.ViewModels;
namespace Wabbajack.App.Blazor;
@ -72,6 +73,7 @@ public partial class App
services.AddBlazoredModal();
services.AddBlazoredToast();
services.AddTransient<MainWindow>();
services.AddTransient<NexusLogin>();
services.AddSingleton<SystemParametersConstructor>();
services.AddSingleton<IStateContainer, StateContainer>();
return services;

View File

@ -0,0 +1,23 @@
<TabItem x:Class="Wabbajack.App.Blazor.Browser.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:local="clr-namespace:Wabbajack.App.Blazor.Browser"
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
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>
<local:BrowserView x:Name="Browser">
</local:BrowserView>
</Grid>
</TabItem>

View File

@ -0,0 +1,51 @@
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.App.Blazor.Browser;
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;
tc.Items.Remove(this);
this.Dispose();
}
}

View File

@ -0,0 +1,84 @@
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.Fody.Helpers;
using Wabbajack.DTOs.Logins;
namespace Wabbajack.App.Blazor.Browser;
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);
}
protected abstract Task Run(CancellationToken token);
public async Task NavigateTo(Uri uri)
{
var tcs = new TaskCompletionSource();
void Completed(object? o, CoreWebView2NavigationCompletedEventArgs a)
{
if (a.IsSuccess)
{
tcs.TrySetResult();
}
else
{
tcs.TrySetException(new Exception($"Navigation error to {uri}"));
}
}
_browser.NavigationCompleted += Completed;
_browser.Source = uri;
await tcs.Task;
_browser.NavigationCompleted -= Completed;
}
public async Task<Cookie[]> GetCookies(string domainEnding, CancellationToken token)
{
var cookies = (await _browser.CoreWebView2.CookieManager.GetCookiesAsync(""))
.Where(c => c.Domain.EndsWith(domainEnding));
return cookies.Select(c => new Cookie
{
Domain = c.Domain,
Name = c.Name,
Path = c.Path,
Value = c.Value
}).ToArray();
}
public async Task<string> EvaluateJavaScript(string js)
{
return await _browser.ExecuteScriptAsync(js);
}
public async Task<HtmlDocument> GetDom(CancellationToken token)
{
var v = HttpUtility.UrlDecode("\u003D");
var source = await EvaluateJavaScript("document.body.outerHTML");
var decoded = JsonSerializer.Deserialize<string>(source);
var doc = new HtmlDocument();
doc.LoadHtml(decoded);
return doc;
}
}

View File

@ -0,0 +1,34 @@
<reactiveUi:ReactiveUserControl x:TypeArguments="local:BrowserTabViewModel"
x:Class="Wabbajack.App.Blazor.Browser.BrowserView"
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.Blazor.Browser"
xmlns:wpf="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
xmlns:reactiveUi="http://reactiveui.net"
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30"></ColumnDefinition>
<ColumnDefinition Width="30"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="30"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Button Grid.Row="0" Grid.Column="0">
<iconPacks:PackIconModern Kind="NavigatePrevious"></iconPacks:PackIconModern>
</Button>
<Button Grid.Row="0" Grid.Column="1">
<iconPacks:PackIconModern Kind="Home"></iconPacks:PackIconModern>
</Button>
<TextBox Grid.Row="0" Grid.Column="3" VerticalContentAlignment="Center"></TextBox>
<wpf:WebView2 Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" x:Name="Browser"></wpf:WebView2>
</Grid>
</reactiveUi:ReactiveUserControl>

View File

@ -0,0 +1,13 @@
using System.Windows.Controls;
using ReactiveUI;
namespace Wabbajack.App.Blazor.Browser;
public partial class BrowserView
{
public BrowserView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,8 @@
using ReactiveUI;
namespace Wabbajack.App.Blazor.Browser;
public class ViewModel : ReactiveObject, IActivatableViewModel
{
public ViewModelActivator Activator { get; } = new();
}

View File

@ -0,0 +1,94 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Fizzler.Systems.HtmlAgilityPack;
using Wabbajack.DTOs.Logins;
using Wabbajack.Services.OSIntegrated;
namespace Wabbajack.App.Blazor.Browser.ViewModels;
public class NexusLogin : BrowserTabViewModel
{
private readonly EncryptedJsonTokenProvider<NexusApiState> _tokenProvider;
public NexusLogin(EncryptedJsonTokenProvider<NexusApiState> tokenProvider)
{
HeaderText = "Nexus Login";
_tokenProvider = tokenProvider;
}
protected override async Task Run(CancellationToken token)
{
token.ThrowIfCancellationRequested();
Instructions = "Please log into the Nexus";
await NavigateTo(new Uri(
"https://users.nexusmods.com/auth/continue?client_id=nexus&redirect_uri=https://www.nexusmods.com/oauth/callback&response_type=code&referrer=//www.nexusmods.com"));
Cookie[] cookies = { };
while (true)
{
cookies = await GetCookies("nexusmods.com", token);
if (cookies.Any(c => c.Name == "member_id"))
break;
token.ThrowIfCancellationRequested();
await Task.Delay(500, token);
}
Instructions = "Getting API Key...";
await NavigateTo(new Uri("https://www.nexusmods.com/users/myaccount?tab=api"));
var key = "";
while (true)
{
try
{
key = (await GetDom(token))
.DocumentNode
.QuerySelectorAll("input[value=wabbajack]")
.SelectMany(p => p.ParentNode.ParentNode.QuerySelectorAll("textarea.application-key"))
.Select(node => node.InnerHtml)
.FirstOrDefault() ?? "";
}
catch (Exception)
{
// ignored
}
if (!string.IsNullOrEmpty(key))
break;
try
{
await EvaluateJavaScript(
"var found = document.querySelector(\"input[value=wabbajack]\").parentElement.parentElement.querySelector(\"form button[type=submit]\");" +
"found.onclick= function() {return true;};" +
"found.class = \" \"; " +
"found.click();" +
"found.remove(); found = undefined;"
);
Instructions = "Generating API Key, Please Wait...";
}
catch (Exception)
{
// ignored
}
token.ThrowIfCancellationRequested();
await Task.Delay(500, token);
}
Instructions = "Success, saving information...";
await _tokenProvider.SetToken(new NexusApiState
{
Cookies = cookies,
ApiKey = key
});
}
}

View File

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ReactiveUI />
</Weavers>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="ReactiveUI" minOccurs="0" maxOccurs="1" type="xs:anyType" />
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

View File

@ -1,20 +1,29 @@
<Window x:Class="Wabbajack.App.Blazor.MainWindow"
<mah:MetroWindow x:Class="Wabbajack.App.Blazor.MainWindow"
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Wabbajack.App.Blazor"
xmlns:blazor="clr-namespace:Microsoft.AspNetCore.Components.WebView.Wpf;assembly=Microsoft.AspNetCore.Components.WebView.Wpf"
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
mc:Ignorable="d"
Title="MainWindow" Height="750" Width="1200" MinHeight="750" MinWidth="1200">
<Grid Background="#121212">
<blazor:BlazorWebView HostPage="wwwroot\index.html" x:Name="BlazorWebView">
<blazor:BlazorWebView.RootComponents>
<blazor:RootComponent Selector="#app" ComponentType="{x:Type local:Main}" />
</blazor:BlazorWebView.RootComponents>
</blazor:BlazorWebView>
ShowTitleBar="False"
Title="WABBAJACK" Height="750" Width="1200" MinHeight="750" MinWidth="1200">
<Grid Background="#121212" MouseDown="UIElement_OnMouseDown">
<TabControl x:Name="Tabs">
<TabItem>
<TabItem.Header>
<TextBlock FontSize="16" Margin="0, 0, 8, 0">WABBAJACK 3.0.0</TextBlock>
</TabItem.Header>
<blazor:BlazorWebView HostPage="wwwroot\index.html" x:Name="BlazorWebView">
<blazor:BlazorWebView.RootComponents>
<blazor:RootComponent Selector="#app" ComponentType="{x:Type local:Main}" />
</blazor:BlazorWebView.RootComponents>
</blazor:BlazorWebView>
</TabItem>
</TabControl>
</Grid>
<Window.TaskbarItemInfo>
<TaskbarItemInfo x:Name="TaskBarItem"/>
</Window.TaskbarItemInfo>
</Window>
</mah:MetroWindow>

View File

@ -1,12 +1,24 @@
using System;
using System.Reactive.Disposables;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using ReactiveUI;
using Wabbajack.App.Blazor.Browser;
using Wabbajack.App.Blazor.Messages;
using Wabbajack.App.Blazor.State;
namespace Wabbajack.App.Blazor;
public partial class MainWindow
public partial class MainWindow : IDisposable
{
private Point _lastPosition;
private readonly CompositeDisposable _compositeDisposable;
public MainWindow(IServiceProvider serviceProvider, IStateContainer stateContainer)
{
_compositeDisposable = new CompositeDisposable();
stateContainer.TaskBarStateObservable.Subscribe(state =>
{
Dispatcher.InvokeAsync(() =>
@ -16,10 +28,31 @@ public partial class MainWindow
TaskBarItem.ProgressValue = state.ProgressValue;
});
});
MessageBus.Current.Listen<OpenBrowserTab>()
.Subscribe(OnOpenBrowserTab)
.DisposeWith(_compositeDisposable);
InitializeComponent();
BlazorWebView.Services = serviceProvider;
}
private void UIElement_OnMouseDown(object sender, MouseButtonEventArgs e)
{
this.DragMove();
}
private void OnOpenBrowserTab(OpenBrowserTab msg)
{
var tab = new BrowserTabView(msg.ViewModel);
Tabs.Items.Add(tab);
Tabs.SelectedItem = tab;
}
public void Dispose()
{
_compositeDisposable.Dispose();
}
}
// Required so compiler doesn't complain about not finding the type. [MC3050]

View File

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

View File

@ -1,12 +1,24 @@
@page "/settings"
@using ReactiveUI
@using Wabbajack.App.Blazor.Browser.ViewModels
@using Wabbajack.App.Blazor.Messages
@using Microsoft.Extensions.DependencyInjection
@namespace Wabbajack.App.Blazor.Pages
@inject IServiceProvider _serviceProvider;
<div id="content">
<div class="resources">
<button onclick="@LoginToNexus">Login To Nexus</button>
</div>
</div>
@code {
public const string Route = "/settings";
public void LoginToNexus()
{
MessageBus.Current.SendMessage(new OpenBrowserTab(_serviceProvider.GetRequiredService<NexusLogin>()));
}
}

View File

@ -17,7 +17,10 @@
<PackageReference Include="Blazored.Modal" Version="6.0.1" />
<PackageReference Include="Blazored.Toast" Version="3.2.2" />
<PackageReference Include="DynamicData" Version="7.4.9" />
<PackageReference Include="Fizzler.Systems.HtmlAgilityPack" Version="1.2.1" />
<PackageReference Include="GitInfo" Version="2.2.0" />
<PackageReference Include="MahApps.Metro" Version="2.4.9" />
<PackageReference Include="MahApps.Metro.IconPacks" Version="4.11.0" />
<PackageReference Include="Microsoft-WindowsAPICodePack-Shell" Version="1.1.4" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Wpf" Version="6.0.101-preview.11.2349" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="6.0.0" />
@ -25,6 +28,9 @@
<PackageReference Include="NLog" Version="4.7.13" />
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
<PackageReference Include="PInvoke.User32" Version="0.7.104" />
<PackageReference Include="ReactiveUI" Version="17.1.17" />
<PackageReference Include="ReactiveUI.Fody" Version="17.1.17" />
<PackageReference Include="ReactiveUI.WPF" Version="17.1.17" />
<PackageReference Include="Silk.NET.DXGI" Version="2.12.0" />
<PackageReference Include="System.Reactive" Version="5.0.0" />
</ItemGroup>
@ -47,6 +53,10 @@
<ProjectReference Include="..\Wabbajack.Services.OSIntegrated\Wabbajack.Services.OSIntegrated.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Browser\BrowserWindow.xaml.cs" />
</ItemGroup>
<!-- dotnet tool install Excubo.WebCompiler -g -->
<Target Name="TestWebCompiler" BeforeTargets="PreBuildEvent">
<Exec Command="webcompiler -h" ContinueOnError="true" StandardOutputImportance="low" StandardErrorImportance="low" LogStandardErrorAsError="false" IgnoreExitCode="true">

View File

@ -113,7 +113,7 @@ public static class ServiceExtensions
service.AddSingleton<WriteOnlyClient>();
// Token Providers
service.AddAllSingleton<ITokenProvider<NexusApiState>, NexusApiTokenProvider>();
service.AddAllSingleton<ITokenProvider<NexusApiState>, EncryptedJsonTokenProvider<NexusApiState>, NexusApiTokenProvider>();
service
.AddAllSingleton<ITokenProvider<LoversLabLoginState>, EncryptedJsonTokenProvider<LoversLabLoginState>,
LoversLabTokenProvider>();