mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
Code cleanup and fix test running
This commit is contained in:
parent
2b5662a15b
commit
f99f4a7538
2
.github/workflows/tests.yaml
vendored
2
.github/workflows/tests.yaml
vendored
@ -43,7 +43,7 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: dotnet build --configuration Release --no-restore
|
run: dotnet build --configuration Release --no-restore
|
||||||
- name: Test
|
- name: Test
|
||||||
run: dotnet test --no-restore --filter "Category=!FlakeyNetwork"
|
run: dotnet test --no-restore --filter "Category!=FlakeyNetwork"
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
name: Publish Projects
|
name: Publish Projects
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using ReactiveUI;
|
|
||||||
using Wabbajack.App.Models;
|
using Wabbajack.App.Models;
|
||||||
|
|
||||||
namespace Wabbajack.App.Test;
|
namespace Wabbajack.App.Test;
|
||||||
@ -28,6 +27,7 @@ public static class Extensions
|
|||||||
await Task.Delay(100);
|
await Task.Delay(100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task WaitForUnlock(this LoadingLock l)
|
public static async Task WaitForUnlock(this LoadingLock l)
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.RunJobs();
|
Dispatcher.UIThread.RunJobs();
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Data;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Wabbajack.App.Controls;
|
using Wabbajack.App.Controls;
|
||||||
@ -15,23 +14,21 @@ namespace Wabbajack.App.Test;
|
|||||||
|
|
||||||
public class GalleryItemTests
|
public class GalleryItemTests
|
||||||
{
|
{
|
||||||
private readonly BrowseViewModel _gallery;
|
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
|
private readonly BrowseViewModel _gallery;
|
||||||
|
|
||||||
public GalleryItemTests(BrowseViewModel bvm, Configuration config)
|
public GalleryItemTests(BrowseViewModel bvm, Configuration config)
|
||||||
{
|
{
|
||||||
_config = config;
|
_config = config;
|
||||||
_gallery = bvm;
|
_gallery = bvm;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CanDownloadGalleryItem()
|
public async Task CanDownloadGalleryItem()
|
||||||
{
|
{
|
||||||
foreach (var file in _config.ModListsDownloadLocation.EnumerateFiles().Where(f => f.Extension == Ext.Wabbajack))
|
foreach (var file in _config.ModListsDownloadLocation.EnumerateFiles().Where(f => f.Extension == Ext.Wabbajack))
|
||||||
{
|
|
||||||
file.Delete();
|
file.Delete();
|
||||||
}
|
|
||||||
|
|
||||||
using var _ = _gallery.Activator.Activate();
|
using var _ = _gallery.Activator.Activate();
|
||||||
await _gallery.LoadingLock.WaitForLock();
|
await _gallery.LoadingLock.WaitForLock();
|
||||||
await _gallery.LoadingLock.WaitForUnlock();
|
await _gallery.LoadingLock.WaitForUnlock();
|
||||||
@ -44,7 +41,7 @@ public class GalleryItemTests
|
|||||||
Assert.True(item.ModListLocation.FileExists());
|
Assert.True(item.ModListLocation.FileExists());
|
||||||
else
|
else
|
||||||
Assert.False(item.ModListLocation.FileExists());
|
Assert.False(item.ModListLocation.FileExists());
|
||||||
|
|
||||||
Assert.Equal(Percent.Zero, item.Progress);
|
Assert.Equal(Percent.Zero, item.Progress);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,18 +55,18 @@ public class GalleryItemTests
|
|||||||
Assert.True(modList.Progress >= progress);
|
Assert.True(modList.Progress >= progress);
|
||||||
progress = modList.Progress;
|
progress = modList.Progress;
|
||||||
});
|
});
|
||||||
|
|
||||||
Assert.Equal(Percent.Zero, modList.Progress);
|
Assert.Equal(Percent.Zero, modList.Progress);
|
||||||
Assert.Equal(ModListState.Downloaded, modList.State);
|
Assert.Equal(ModListState.Downloaded, modList.State);
|
||||||
|
|
||||||
|
|
||||||
modList.ExecuteCommand.Execute().Subscribe().Dispose();
|
modList.ExecuteCommand.Execute().Subscribe().Dispose();
|
||||||
|
|
||||||
var msgs = ((SimpleMessageBus) MessageBus.Instance).Messages.TakeLast(2).ToArray();
|
var msgs = ((SimpleMessageBus) MessageBus.Instance).Messages.TakeLast(2).ToArray();
|
||||||
|
|
||||||
var configure = msgs.OfType<StartInstallConfiguration>().First();
|
var configure = msgs.OfType<StartInstallConfiguration>().First();
|
||||||
Assert.Equal(modList.ModListLocation, configure.ModList);
|
Assert.Equal(modList.ModListLocation, configure.ModList);
|
||||||
|
|
||||||
var navigate = msgs.OfType<NavigateTo>().First();
|
var navigate = msgs.OfType<NavigateTo>().First();
|
||||||
Assert.Equal(typeof(InstallConfigurationViewModel), navigate.ViewModel);
|
Assert.Equal(typeof(InstallConfigurationViewModel), navigate.ViewModel);
|
||||||
}
|
}
|
||||||
|
@ -1,35 +1,31 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Avalonia.Threading;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Wabbajack.App;
|
|
||||||
using Wabbajack.Networking.WabbajackClientApi;
|
|
||||||
using Wabbajack.Services.OSIntegrated;
|
|
||||||
using Xunit.DependencyInjection;
|
using Xunit.DependencyInjection;
|
||||||
using Xunit.DependencyInjection.Logging;
|
using Xunit.DependencyInjection.Logging;
|
||||||
|
|
||||||
namespace Wabbajack.App.Test
|
namespace Wabbajack.App.Test;
|
||||||
{
|
|
||||||
public class Startup
|
|
||||||
{
|
|
||||||
public void ConfigureServices(IServiceCollection service)
|
|
||||||
{
|
|
||||||
service.AddAppServices();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Configure(ILoggerFactory loggerFactory, ITestOutputHelperAccessor accessor)
|
public class Startup
|
||||||
{
|
{
|
||||||
loggerFactory.AddProvider(new XunitTestOutputLoggerProvider(accessor, delegate { return true; }));
|
public void ConfigureServices(IServiceCollection service)
|
||||||
MessageBus.Instance = new SimpleMessageBus();
|
{
|
||||||
}
|
service.AddAppServices();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SimpleMessageBus : IMessageBus
|
public void Configure(ILoggerFactory loggerFactory, ITestOutputHelperAccessor accessor)
|
||||||
{
|
{
|
||||||
public List<object> Messages { get; } = new();
|
loggerFactory.AddProvider(new XunitTestOutputLoggerProvider(accessor, delegate { return true; }));
|
||||||
public void Send<T>(T message)
|
MessageBus.Instance = new SimpleMessageBus();
|
||||||
{
|
}
|
||||||
Messages.Add(message);
|
}
|
||||||
}
|
|
||||||
|
public class SimpleMessageBus : IMessageBus
|
||||||
|
{
|
||||||
|
public List<object> Messages { get; } = new();
|
||||||
|
|
||||||
|
public void Send<T>(T message)
|
||||||
|
{
|
||||||
|
Messages.Add(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,21 +7,21 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0"/>
|
||||||
<PackageReference Include="xunit" Version="2.4.1" />
|
<PackageReference Include="xunit" Version="2.4.1"/>
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Xunit.DependencyInjection" Version="7.7.0" />
|
<PackageReference Include="Xunit.DependencyInjection" Version="7.7.0"/>
|
||||||
<PackageReference Include="Xunit.DependencyInjection.Logging" Version="7.5.1" />
|
<PackageReference Include="Xunit.DependencyInjection.Logging" Version="7.5.1"/>
|
||||||
<PackageReference Include="coverlet.collector" Version="3.1.0">
|
<PackageReference Include="coverlet.collector" Version="3.1.0">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Wabbajack.App\Wabbajack.App.csproj" />
|
<ProjectReference Include="..\Wabbajack.App\Wabbajack.App.csproj"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -1,25 +1,24 @@
|
|||||||
<Application xmlns="https://github.com/avaloniaui"
|
<Application xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:local="using:Wabbajack.App"
|
xmlns:local="using:Wabbajack.App"
|
||||||
xmlns:i="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
|
||||||
x:Class="Wabbajack.App.App">
|
x:Class="Wabbajack.App.App">
|
||||||
<Application.DataTemplates>
|
<Application.DataTemplates>
|
||||||
<local:ViewLocator/>
|
<local:ViewLocator />
|
||||||
</Application.DataTemplates>
|
</Application.DataTemplates>
|
||||||
|
|
||||||
<Application.Styles>
|
<Application.Styles>
|
||||||
<StyleInclude Source="avares://Material.Icons.Avalonia/App.xaml"></StyleInclude>
|
<StyleInclude Source="avares://Material.Icons.Avalonia/App.xaml" />
|
||||||
<FluentTheme Mode="Dark"/>
|
<FluentTheme Mode="Dark" />
|
||||||
<StyleInclude Source="avares://Wabbajack.App/Assets/Wabbajack.axaml"></StyleInclude>
|
<StyleInclude Source="avares://Wabbajack.App/Assets/Wabbajack.axaml" />
|
||||||
<Style Selector="Button:not(:pointerover) /template/ ContentPresenter">
|
<Style Selector="Button:not(:pointerover) /template/ ContentPresenter">
|
||||||
<Setter Property="Background" Value="Transparent"></Setter>
|
<Setter Property="Background" Value="Transparent" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
|
||||||
<Style Selector="Button:pointerover /template/ ContentPresenter">
|
<Style Selector="Button:pointerover /template/ ContentPresenter">
|
||||||
<Setter Property="Background" Value="Transparent"></Setter>
|
<Setter Property="Background" Value="Transparent" />
|
||||||
<Setter Property="BorderBrush" Value="LightGray"></Setter>
|
<Setter Property="BorderBrush" Value="LightGray" />
|
||||||
<Setter Property="CornerRadius" Value="5"></Setter>
|
<Setter Property="CornerRadius" Value="5" />
|
||||||
</Style>
|
</Style>
|
||||||
</Application.Styles>
|
</Application.Styles>
|
||||||
</Application>
|
</Application>
|
@ -3,80 +3,66 @@ using Avalonia;
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using CefNet;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using Splat;
|
using Splat;
|
||||||
using Wabbajack.App.Controls;
|
|
||||||
using Wabbajack.App.Converters;
|
using Wabbajack.App.Converters;
|
||||||
using Wabbajack.App.Interfaces;
|
|
||||||
using Wabbajack.App.Models;
|
|
||||||
using Wabbajack.App.Utilities;
|
using Wabbajack.App.Utilities;
|
||||||
using Wabbajack.App.ViewModels;
|
|
||||||
using Wabbajack.App.Views;
|
using Wabbajack.App.Views;
|
||||||
using Wabbajack.DTOs.JsonConverters;
|
|
||||||
using Wabbajack.Networking.NexusApi;
|
|
||||||
using Wabbajack.Services.OSIntegrated;
|
|
||||||
|
|
||||||
namespace Wabbajack.App
|
namespace Wabbajack.App;
|
||||||
|
|
||||||
|
public class App : Application
|
||||||
{
|
{
|
||||||
public class App : Application
|
public static IServiceProvider Services { get; private set; } = null!;
|
||||||
|
public static Window? MainWindow { get; set; }
|
||||||
|
|
||||||
|
public static event EventHandler FrameworkInitialized;
|
||||||
|
public static event EventHandler FrameworkShutdown;
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
public static event EventHandler FrameworkInitialized;
|
}
|
||||||
public static event EventHandler FrameworkShutdown;
|
|
||||||
public static IServiceProvider Services { get; private set; } = null!;
|
public override void OnFrameworkInitializationCompleted()
|
||||||
public static Window? MainWindow { get; set; }
|
{
|
||||||
public override void Initialize()
|
var host = Host.CreateDefaultBuilder(Array.Empty<string>())
|
||||||
|
.ConfigureLogging(c => { c.ClearProviders(); })
|
||||||
|
.ConfigureServices((host, services) => { services.AddAppServices(); }).Build();
|
||||||
|
Services = host.Services;
|
||||||
|
|
||||||
|
SetupConverters();
|
||||||
|
|
||||||
|
// Need to startup the message bus;
|
||||||
|
Services.GetService<MessageBus>();
|
||||||
|
var app = Services.GetService<CefAppImpl>();
|
||||||
|
|
||||||
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
{
|
{
|
||||||
AvaloniaXamlLoader.Load(this);
|
desktop.MainWindow = new MainWindow();
|
||||||
|
desktop.Startup += Startup;
|
||||||
|
desktop.Exit += Exit;
|
||||||
|
MainWindow = desktop.MainWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnFrameworkInitializationCompleted()
|
base.OnFrameworkInitializationCompleted();
|
||||||
{
|
}
|
||||||
var host = Host.CreateDefaultBuilder(Array.Empty<string>())
|
|
||||||
.ConfigureLogging(c =>
|
|
||||||
{
|
|
||||||
c.ClearProviders();
|
|
||||||
})
|
|
||||||
.ConfigureServices((host, services) =>
|
|
||||||
{
|
|
||||||
services.AddAppServices();
|
|
||||||
}).Build();
|
|
||||||
Services = host.Services;
|
|
||||||
|
|
||||||
SetupConverters();
|
private void Startup(object sender, ControlledApplicationLifetimeStartupEventArgs e)
|
||||||
|
{
|
||||||
|
FrameworkInitialized?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
// Need to startup the message bus;
|
private void Exit(object sender, ControlledApplicationLifetimeExitEventArgs e)
|
||||||
Services.GetService<MessageBus>();
|
{
|
||||||
var app = Services.GetService<CefAppImpl>();
|
FrameworkShutdown?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
private void SetupConverters()
|
||||||
{
|
{
|
||||||
desktop.MainWindow = new MainWindow();
|
Locator.CurrentMutable.RegisterConstant<IBindingTypeConverter>(new AbsoultePathBindingConverter());
|
||||||
desktop.Startup += Startup;
|
|
||||||
desktop.Exit += Exit;
|
|
||||||
MainWindow = desktop.MainWindow;
|
|
||||||
}
|
|
||||||
|
|
||||||
base.OnFrameworkInitializationCompleted();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Startup(object sender, ControlledApplicationLifetimeStartupEventArgs e)
|
|
||||||
{
|
|
||||||
FrameworkInitialized?.Invoke(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Exit(object sender, ControlledApplicationLifetimeExitEventArgs e)
|
|
||||||
{
|
|
||||||
FrameworkShutdown?.Invoke(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetupConverters()
|
|
||||||
{
|
|
||||||
Locator.CurrentMutable.RegisterConstant<IBindingTypeConverter>(new AbsoultePathBindingConverter());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,36 +7,33 @@
|
|||||||
</Border>
|
</Border>
|
||||||
</Design.PreviewWith>
|
</Design.PreviewWith>
|
||||||
|
|
||||||
|
|
||||||
<Style Selector="controls|TagView Border">
|
<Style Selector="controls|TagView Border">
|
||||||
<Setter Property="BorderThickness" Value="1"></Setter>
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="BorderBrush" Value="#121212"></Setter>
|
<Setter Property="BorderBrush" Value="#121212" />
|
||||||
<Setter Property="CornerRadius" Value="5"></Setter>
|
<Setter Property="CornerRadius" Value="5" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
|
||||||
<Style Selector="controls|TagView.ModList Border">
|
<Style Selector="controls|TagView.ModList Border">
|
||||||
<Setter Property="Background" Value="#868CFC"></Setter>
|
<Setter Property="Background" Value="#868CFC" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="controls|TagView.Game Border">
|
|
||||||
<Setter Property="Background" Value="#F686FC"></Setter>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style Selector="controls|TagView.GameNotInstalled Border">
|
|
||||||
<Setter Property="Background" Value="#FCBB86"></Setter>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style Selector="controls|TagView.GameNotInstalled Border">
|
|
||||||
<Setter Property="Background" Value="#FCBB86"></Setter>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style Selector="controls|TagView TextBlock">
|
|
||||||
<Setter Property="Foreground" Value="#121212"></Setter>
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<Style Selector="controls|TagView.Game Border">
|
||||||
|
<Setter Property="Background" Value="#F686FC" />
|
||||||
</Styles>
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="controls|TagView.GameNotInstalled Border">
|
||||||
|
<Setter Property="Background" Value="#FCBB86" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="controls|TagView.GameNotInstalled Border">
|
||||||
|
<Setter Property="Background" Value="#FCBB86" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="controls|TagView TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="#121212" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
|
||||||
|
</Styles>
|
@ -1 +1,6 @@
|
|||||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="cloud-download-alt" class="svg-inline--fa fa-cloud-download-alt fa-w-20" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M537.6 226.6c4.1-10.7 6.4-22.4 6.4-34.6 0-53-43-96-96-96-19.7 0-38.1 6-53.3 16.2C367 64.2 315.3 32 256 32c-88.4 0-160 71.6-160 160 0 2.7.1 5.4.2 8.1C40.2 219.8 0 273.2 0 336c0 79.5 64.5 144 144 144h368c70.7 0 128-57.3 128-128 0-61.9-44-113.6-102.4-125.4zm-132.9 88.7L299.3 420.7c-6.2 6.2-16.4 6.2-22.6 0L171.3 315.3c-10.1-10.1-2.9-27.3 11.3-27.3H248V176c0-8.8 7.2-16 16-16h48c8.8 0 16 7.2 16 16v112h65.4c14.2 0 21.4 17.2 11.3 27.3z"></path></svg>
|
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="cloud-download-alt"
|
||||||
|
class="svg-inline--fa fa-cloud-download-alt fa-w-20" role="img" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 640 512">
|
||||||
|
<path fill="currentColor"
|
||||||
|
d="M537.6 226.6c4.1-10.7 6.4-22.4 6.4-34.6 0-53-43-96-96-96-19.7 0-38.1 6-53.3 16.2C367 64.2 315.3 32 256 32c-88.4 0-160 71.6-160 160 0 2.7.1 5.4.2 8.1C40.2 219.8 0 273.2 0 336c0 79.5 64.5 144 144 144h368c70.7 0 128-57.3 128-128 0-61.9-44-113.6-102.4-125.4zm-132.9 88.7L299.3 420.7c-6.2 6.2-16.4 6.2-22.6 0L171.3 315.3c-10.1-10.1-2.9-27.3 11.3-27.3H248V176c0-8.8 7.2-16 16-16h48c8.8 0 16 7.2 16 16v112h65.4c14.2 0 21.4 17.2 11.3 27.3z"></path>
|
||||||
|
</svg>
|
Before Width: | Height: | Size: 687 B After Width: | Height: | Size: 713 B |
@ -1,14 +1,13 @@
|
|||||||
using Wabbajack.Paths;
|
using Wabbajack.Paths;
|
||||||
|
|
||||||
namespace Wabbajack.App
|
namespace Wabbajack.App;
|
||||||
|
|
||||||
|
public class Configuration
|
||||||
{
|
{
|
||||||
public class Configuration
|
public AbsolutePath ModListsDownloadLocation { get; set; }
|
||||||
{
|
public AbsolutePath SavedSettingsLocation { get; set; }
|
||||||
public AbsolutePath ModListsDownloadLocation { get; set; }
|
|
||||||
public AbsolutePath SavedSettingsLocation { get; set; }
|
public AbsolutePath EncryptedDataLocation { get; set; }
|
||||||
|
|
||||||
public AbsolutePath EncryptedDataLocation { get; set; }
|
public AbsolutePath LogLocation { get; set; }
|
||||||
|
|
||||||
public AbsolutePath LogLocation { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -42,12 +42,12 @@
|
|||||||
<ItemsControl Grid.Row="2" x:Name="TagsList">
|
<ItemsControl Grid.Row="2" x:Name="TagsList">
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsControl.ItemsPanel>
|
||||||
<ItemsPanelTemplate>
|
<ItemsPanelTemplate>
|
||||||
<WrapPanel/>
|
<WrapPanel />
|
||||||
</ItemsPanelTemplate>
|
</ItemsPanelTemplate>
|
||||||
</ItemsControl.ItemsPanel>
|
</ItemsControl.ItemsPanel>
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<controls:TagView></controls:TagView>
|
<controls:TagView />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
@ -89,7 +89,7 @@
|
|||||||
<avalonia:MaterialIcon
|
<avalonia:MaterialIcon
|
||||||
Width="20"
|
Width="20"
|
||||||
Height="20"
|
Height="20"
|
||||||
x:Name="ExecuteIcon"/>
|
x:Name="ExecuteIcon" />
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -1,56 +1,51 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
using Material.Icons;
|
using Material.Icons;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using Wabbajack.App.Views;
|
|
||||||
|
|
||||||
namespace Wabbajack.App.Controls
|
namespace Wabbajack.App.Controls;
|
||||||
|
|
||||||
|
public partial class BrowseItemView : ReactiveUserControl<BrowseItemViewModel>
|
||||||
{
|
{
|
||||||
public partial class BrowseItemView : ReactiveUserControl<BrowseItemViewModel>
|
public BrowseItemView()
|
||||||
{
|
{
|
||||||
public BrowseItemView()
|
InitializeComponent();
|
||||||
|
|
||||||
|
this.WhenActivated(disposables =>
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
this.OneWayBind(ViewModel, vm => vm.Title, view => view.Title.Text)
|
||||||
|
.DisposeWith(disposables);
|
||||||
|
this.OneWayBind(ViewModel, vm => vm.Description, view => view.Description.Text)
|
||||||
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.WhenActivated(disposables =>
|
this.OneWayBind(ViewModel, vm => vm.Image, view => view.ModListImage.Source)
|
||||||
{
|
.DisposeWith(disposables);
|
||||||
this.OneWayBind(ViewModel, vm => vm.Title, view => view.Title.Text)
|
|
||||||
.DisposeWith(disposables);
|
|
||||||
this.OneWayBind(ViewModel, vm => vm.Description, view => view.Description.Text)
|
|
||||||
.DisposeWith(disposables);
|
|
||||||
|
|
||||||
this.OneWayBind(ViewModel, vm => vm.Image, view => view.ModListImage.Source)
|
this.BindCommand(ViewModel, vm => vm.OpenWebsiteCommand, view => view.OpenWebsiteButton)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.BindCommand(ViewModel, vm => vm.OpenWebsiteCommand, view => view.OpenWebsiteButton)
|
this.OneWayBind(ViewModel, vm => vm.State, view => view.ExecuteIcon.Kind, s => StateToKind(s));
|
||||||
.DisposeWith(disposables);
|
this.BindCommand(ViewModel, vm => vm.ExecuteCommand, view => view.ExecuteButton)
|
||||||
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.OneWayBind(ViewModel, vm => vm.State, view => view.ExecuteIcon.Kind, s => StateToKind(s));
|
this.OneWayBind(ViewModel, vm => vm.Progress, view => view.DownloadProgressBar.Value,
|
||||||
this.BindCommand(ViewModel, vm => vm.ExecuteCommand, view => view.ExecuteButton)
|
s => s.Value * 1000)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.OneWayBind(ViewModel, vm => vm.Progress, view => view.DownloadProgressBar.Value,
|
this.OneWayBind(ViewModel, vm => vm.Tags, view => view.TagsList.Items)
|
||||||
s => s.Value * 1000)
|
.DisposeWith(disposables);
|
||||||
.DisposeWith(disposables);
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.OneWayBind(ViewModel, vm => vm.Tags, view => view.TagsList.Items)
|
private MaterialIconKind StateToKind(ModListState modListState)
|
||||||
.DisposeWith(disposables);
|
{
|
||||||
});
|
return modListState switch
|
||||||
}
|
|
||||||
|
|
||||||
private MaterialIconKind StateToKind(ModListState modListState)
|
|
||||||
{
|
{
|
||||||
return modListState switch
|
ModListState.Downloaded => MaterialIconKind.PlayArrow,
|
||||||
{
|
ModListState.Downloading => MaterialIconKind.LocalAreaNetworkPending,
|
||||||
ModListState.Downloaded => MaterialIconKind.PlayArrow,
|
ModListState.NotDownloaded => MaterialIconKind.Download,
|
||||||
ModListState.Downloading => MaterialIconKind.LocalAreaNetworkPending,
|
_ => throw new ArgumentOutOfRangeException(nameof(modListState), modListState, null)
|
||||||
ModListState.NotDownloaded => MaterialIconKind.Download,
|
};
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(modListState), modListState, null)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,13 +4,10 @@ using System.Linq;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Reactive;
|
using System.Reactive;
|
||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.VisualBasic.CompilerServices;
|
|
||||||
using Octokit;
|
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using ReactiveUI.Fody.Helpers;
|
using ReactiveUI.Fody.Helpers;
|
||||||
using Wabbajack.App.Messages;
|
using Wabbajack.App.Messages;
|
||||||
@ -20,90 +17,64 @@ using Wabbajack.Downloaders;
|
|||||||
using Wabbajack.Downloaders.GameFile;
|
using Wabbajack.Downloaders.GameFile;
|
||||||
using Wabbajack.DTOs;
|
using Wabbajack.DTOs;
|
||||||
using Wabbajack.DTOs.JsonConverters;
|
using Wabbajack.DTOs.JsonConverters;
|
||||||
using Wabbajack.Installer;
|
|
||||||
using Wabbajack.Paths;
|
using Wabbajack.Paths;
|
||||||
using Wabbajack.Paths.IO;
|
using Wabbajack.Paths.IO;
|
||||||
using Wabbajack.RateLimiter;
|
using Wabbajack.RateLimiter;
|
||||||
using Wabbajack.VFS;
|
using Wabbajack.VFS;
|
||||||
|
|
||||||
namespace Wabbajack.App.Controls
|
namespace Wabbajack.App.Controls;
|
||||||
|
|
||||||
|
public enum ModListState
|
||||||
{
|
{
|
||||||
public enum ModListState
|
Downloaded,
|
||||||
|
NotDownloaded,
|
||||||
|
Downloading
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BrowseItemViewModel : ViewModelBase, IActivatableViewModel
|
||||||
|
{
|
||||||
|
private readonly HttpClient _client;
|
||||||
|
private readonly Configuration _configuration;
|
||||||
|
private readonly DownloadDispatcher _dispatcher;
|
||||||
|
private readonly IResource<DownloadDispatcher> _downloadLimiter;
|
||||||
|
private readonly DTOSerializer _dtos;
|
||||||
|
private readonly FileHashCache _hashCache;
|
||||||
|
private readonly IResource<HttpClient> _limiter;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly ModlistMetadata _metadata;
|
||||||
|
private readonly ModListSummary _summary;
|
||||||
|
|
||||||
|
public BrowseItemViewModel(ModlistMetadata metadata, ModListSummary summary, HttpClient client,
|
||||||
|
IResource<HttpClient> limiter,
|
||||||
|
FileHashCache hashCache, Configuration configuration, DownloadDispatcher dispatcher,
|
||||||
|
IResource<DownloadDispatcher> downloadLimiter, GameLocator gameLocator,
|
||||||
|
DTOSerializer dtos, ILogger logger)
|
||||||
{
|
{
|
||||||
Downloaded,
|
Activator = new ViewModelActivator();
|
||||||
NotDownloaded,
|
_metadata = metadata;
|
||||||
Downloading
|
_summary = summary;
|
||||||
}
|
_client = client;
|
||||||
|
_limiter = limiter;
|
||||||
|
_hashCache = hashCache;
|
||||||
|
_configuration = configuration;
|
||||||
|
_dispatcher = dispatcher;
|
||||||
|
_downloadLimiter = downloadLimiter;
|
||||||
|
_logger = logger;
|
||||||
|
_dtos = dtos;
|
||||||
|
|
||||||
public class BrowseItemViewModel : ViewModelBase, IActivatableViewModel
|
var haveGame = gameLocator.IsInstalled(_metadata.Game);
|
||||||
{
|
Tags = metadata.tags
|
||||||
private readonly ModlistMetadata _metadata;
|
.Select(t => new TagViewModel(t, "ModList"))
|
||||||
private readonly ModListSummary _summary;
|
.Prepend(new TagViewModel(_metadata.Game.MetaData().HumanFriendlyGameName,
|
||||||
private readonly HttpClient _client;
|
haveGame ? "Game" : "GameNotInstalled"))
|
||||||
private readonly IResource<HttpClient> _limiter;
|
.ToArray();
|
||||||
private readonly FileHashCache _hashCache;
|
|
||||||
private readonly Configuration _configuration;
|
|
||||||
private readonly DownloadDispatcher _dispatcher;
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
private readonly IResource<DownloadDispatcher> _downloadLimiter;
|
|
||||||
private readonly DTOSerializer _dtos;
|
|
||||||
|
|
||||||
public string Title => _metadata.ImageContainsTitle ? "" : _metadata.Title;
|
OpenWebsiteCommand = ReactiveCommand.Create(() =>
|
||||||
public string MachineURL => _metadata.Links.MachineURL;
|
|
||||||
public string Description => _metadata.Description;
|
|
||||||
|
|
||||||
public Uri ImageUri => new(_metadata.Links.ImageUri);
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public IBitmap Image { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public ModListState State { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public ReactiveCommand<Unit,Unit> ExecuteCommand { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public Percent Progress { get; set; }
|
|
||||||
|
|
||||||
public AbsolutePath ModListLocation => _configuration.ModListsDownloadLocation.Combine(_metadata.Links.MachineURL).WithExtension(Ext.Wabbajack);
|
|
||||||
|
|
||||||
public Game Game => _metadata.Game;
|
|
||||||
|
|
||||||
public bool IsUtilityList => _metadata.UtilityList;
|
|
||||||
public bool IsNSFW => _metadata.NSFW;
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public TagViewModel[] Tags { get; set; }
|
|
||||||
|
|
||||||
public BrowseItemViewModel(ModlistMetadata metadata, ModListSummary summary, HttpClient client, IResource<HttpClient> limiter,
|
|
||||||
FileHashCache hashCache, Configuration configuration, DownloadDispatcher dispatcher, IResource<DownloadDispatcher> downloadLimiter, GameLocator gameLocator,
|
|
||||||
DTOSerializer dtos, ILogger logger)
|
|
||||||
{
|
{
|
||||||
Activator = new ViewModelActivator();
|
Utils.OpenWebsiteInExternalBrowser(new Uri(_metadata.Links.Readme));
|
||||||
_metadata = metadata;
|
});
|
||||||
_summary = summary;
|
|
||||||
_client = client;
|
|
||||||
_limiter = limiter;
|
|
||||||
_hashCache = hashCache;
|
|
||||||
_configuration = configuration;
|
|
||||||
_dispatcher = dispatcher;
|
|
||||||
_downloadLimiter = downloadLimiter;
|
|
||||||
_logger = logger;
|
|
||||||
_dtos = dtos;
|
|
||||||
|
|
||||||
var haveGame = gameLocator.IsInstalled(_metadata.Game);
|
|
||||||
Tags = metadata.tags
|
|
||||||
.Select(t => new TagViewModel(t, "ModList"))
|
|
||||||
.Prepend(new TagViewModel(_metadata.Game.MetaData().HumanFriendlyGameName, haveGame ? "Game" : "GameNotInstalled"))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
OpenWebsiteCommand = ReactiveCommand.Create(() =>
|
|
||||||
{
|
|
||||||
Utils.OpenWebsiteInExternalBrowser(new Uri(_metadata.Links.Readme));
|
|
||||||
});
|
|
||||||
|
|
||||||
ExecuteCommand = ReactiveCommand.Create(() =>
|
ExecuteCommand = ReactiveCommand.Create(() =>
|
||||||
{
|
{
|
||||||
if (State == ModListState.Downloaded)
|
if (State == ModListState.Downloaded)
|
||||||
{
|
{
|
||||||
@ -114,80 +85,103 @@ namespace Wabbajack.App.Controls
|
|||||||
{
|
{
|
||||||
DownloadModList().FireAndForget();
|
DownloadModList().FireAndForget();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
this.ObservableForProperty(t => t.State)
|
||||||
|
.Select(c => c.Value != ModListState.Downloading)
|
||||||
|
.StartWith(true));
|
||||||
|
|
||||||
},
|
LoadListImage().FireAndForget();
|
||||||
this.ObservableForProperty(t => t.State)
|
UpdateState().FireAndForget();
|
||||||
.Select(c => c.Value != ModListState.Downloading)
|
}
|
||||||
.StartWith(true));
|
|
||||||
|
|
||||||
LoadListImage().FireAndForget();
|
public string Title => _metadata.ImageContainsTitle ? "" : _metadata.Title;
|
||||||
UpdateState().FireAndForget();
|
public string MachineURL => _metadata.Links.MachineURL;
|
||||||
}
|
public string Description => _metadata.Description;
|
||||||
|
|
||||||
private async Task DownloadModList()
|
public Uri ImageUri => new(_metadata.Links.ImageUri);
|
||||||
|
|
||||||
|
[Reactive] public IBitmap Image { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public ModListState State { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public ReactiveCommand<Unit, Unit> ExecuteCommand { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public Percent Progress { get; set; }
|
||||||
|
|
||||||
|
public AbsolutePath ModListLocation => _configuration.ModListsDownloadLocation.Combine(_metadata.Links.MachineURL)
|
||||||
|
.WithExtension(Ext.Wabbajack);
|
||||||
|
|
||||||
|
public Game Game => _metadata.Game;
|
||||||
|
|
||||||
|
public bool IsUtilityList => _metadata.UtilityList;
|
||||||
|
public bool IsNSFW => _metadata.NSFW;
|
||||||
|
|
||||||
|
[Reactive] public TagViewModel[] Tags { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public ReactiveCommand<Unit, Unit> OpenWebsiteCommand { get; set; }
|
||||||
|
|
||||||
|
private async Task DownloadModList()
|
||||||
|
{
|
||||||
|
State = ModListState.Downloading;
|
||||||
|
var state = _dispatcher.Parse(new Uri(_metadata.Links.Download));
|
||||||
|
var archive = new Archive
|
||||||
{
|
{
|
||||||
State = ModListState.Downloading;
|
State = state!,
|
||||||
var state = _dispatcher.Parse(new Uri(_metadata.Links.Download));
|
Hash = _metadata.DownloadMetadata?.Hash ?? default,
|
||||||
var archive = new Archive
|
Size = _metadata.DownloadMetadata?.Size ?? 0,
|
||||||
{
|
Name = ModListLocation.FileName.ToString()
|
||||||
State = state!,
|
};
|
||||||
Hash = _metadata.DownloadMetadata?.Hash ?? default,
|
|
||||||
Size = _metadata.DownloadMetadata?.Size ?? 0,
|
|
||||||
Name = ModListLocation.FileName.ToString()
|
|
||||||
};
|
|
||||||
|
|
||||||
using var job = await _downloadLimiter.Begin(state!.PrimaryKeyString, archive.Size, CancellationToken.None);
|
using var job = await _downloadLimiter.Begin(state!.PrimaryKeyString, archive.Size, CancellationToken.None);
|
||||||
|
|
||||||
var hashTask = _dispatcher.Download(archive, ModListLocation, job, CancellationToken.None);
|
|
||||||
|
|
||||||
while (!hashTask.IsCompleted)
|
var hashTask = _dispatcher.Download(archive, ModListLocation, job, CancellationToken.None);
|
||||||
{
|
|
||||||
Progress = Percent.FactoryPutInRange(job.Current, job.Size ?? 0);
|
|
||||||
await Task.Delay(100);
|
|
||||||
}
|
|
||||||
|
|
||||||
var hash = await hashTask;
|
|
||||||
if (hash != _metadata.DownloadMetadata?.Hash)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Hash files didn't match after downloading modlist, deleting modlist");
|
|
||||||
if (ModListLocation.FileExists())
|
|
||||||
ModListLocation.Delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
_hashCache.FileHashWriteCache(ModListLocation, hash);
|
|
||||||
|
|
||||||
var metadataPath = ModListLocation.WithExtension(Ext.MetaData);
|
while (!hashTask.IsCompleted)
|
||||||
await metadataPath.WriteAllTextAsync(_dtos.Serialize(_metadata));
|
|
||||||
|
|
||||||
Progress = Percent.Zero;
|
|
||||||
await UpdateState();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public ReactiveCommand<Unit,Unit> OpenWebsiteCommand { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
public async Task LoadListImage()
|
|
||||||
{
|
{
|
||||||
using var job = await _limiter.Begin("Loading modlist image", 0, CancellationToken.None);
|
Progress = Percent.FactoryPutInRange(job.Current, job.Size ?? 0);
|
||||||
var response = await _client.GetByteArrayAsync(ImageUri);
|
await Task.Delay(100);
|
||||||
Image = new Bitmap(new MemoryStream(response));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ModListState> GetState()
|
var hash = await hashTask;
|
||||||
|
if (hash != _metadata.DownloadMetadata?.Hash)
|
||||||
{
|
{
|
||||||
var file = ModListLocation;
|
_logger.LogWarning("Hash files didn't match after downloading modlist, deleting modlist");
|
||||||
if (!file.FileExists())
|
if (ModListLocation.FileExists())
|
||||||
return ModListState.NotDownloaded;
|
ModListLocation.Delete();
|
||||||
|
|
||||||
return (await _hashCache.FileHashCachedAsync(file, CancellationToken.None)) !=
|
|
||||||
_metadata.DownloadMetadata?.Hash ? ModListState.NotDownloaded : ModListState.Downloaded;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateState()
|
_hashCache.FileHashWriteCache(ModListLocation, hash);
|
||||||
{
|
|
||||||
State = await GetState();
|
var metadataPath = ModListLocation.WithExtension(Ext.MetaData);
|
||||||
}
|
await metadataPath.WriteAllTextAsync(_dtos.Serialize(_metadata));
|
||||||
|
|
||||||
|
Progress = Percent.Zero;
|
||||||
|
await UpdateState();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task LoadListImage()
|
||||||
|
{
|
||||||
|
using var job = await _limiter.Begin("Loading modlist image", 0, CancellationToken.None);
|
||||||
|
var response = await _client.GetByteArrayAsync(ImageUri);
|
||||||
|
Image = new Bitmap(new MemoryStream(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ModListState> GetState()
|
||||||
|
{
|
||||||
|
var file = ModListLocation;
|
||||||
|
if (!file.FileExists())
|
||||||
|
return ModListState.NotDownloaded;
|
||||||
|
|
||||||
|
return await _hashCache.FileHashCachedAsync(file, CancellationToken.None) !=
|
||||||
|
_metadata.DownloadMetadata?.Hash
|
||||||
|
? ModListState.NotDownloaded
|
||||||
|
: ModListState.Downloaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateState()
|
||||||
|
{
|
||||||
|
State = await GetState();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,37 +5,37 @@
|
|||||||
xmlns:i="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
xmlns:i="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Wabbajack.App.Controls.FileSelectionBox">
|
x:Class="Wabbajack.App.Controls.FileSelectionBox">
|
||||||
|
|
||||||
<UserControl.Styles>
|
<UserControl.Styles>
|
||||||
<Style Selector="Button:not(:pointerover) /template/ ContentPresenter">
|
<Style Selector="Button:not(:pointerover) /template/ ContentPresenter">
|
||||||
<Setter Property="Background" Value="Transparent"></Setter>
|
<Setter Property="Background" Value="Transparent" />
|
||||||
<Setter Property="BorderBrush" Value="LightGray"></Setter>
|
<Setter Property="BorderBrush" Value="LightGray" />
|
||||||
<Setter Property="CornerRadius" Value="0, 5, 5, 0"></Setter>
|
<Setter Property="CornerRadius" Value="0, 5, 5, 0" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Button:pointerover /template/ ContentPresenter">
|
<Style Selector="Button:pointerover /template/ ContentPresenter">
|
||||||
<Setter Property="Background" Value="Transparent"></Setter>
|
<Setter Property="Background" Value="Transparent" />
|
||||||
<Setter Property="BorderBrush" Value="LightGray"></Setter>
|
<Setter Property="BorderBrush" Value="LightGray" />
|
||||||
<Setter Property="CornerRadius" Value="0, 5, 5, 0"></Setter>
|
<Setter Property="CornerRadius" Value="0, 5, 5, 0" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="TextBox:not(:focus) /template/ ContentPresenter">
|
<Style Selector="TextBox:not(:focus) /template/ ContentPresenter">
|
||||||
<Setter Property="Background" Value="Transparent"></Setter>
|
<Setter Property="Background" Value="Transparent" />
|
||||||
<Setter Property="BorderBrush" Value="LightGray"></Setter>
|
<Setter Property="BorderBrush" Value="LightGray" />
|
||||||
<Setter Property="CornerRadius" Value="5, 0, 0, 5"></Setter>
|
<Setter Property="CornerRadius" Value="5, 0, 0, 5" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="TextBox:focus /template/ ContentPresenter">
|
<Style Selector="TextBox:focus /template/ ContentPresenter">
|
||||||
<Setter Property="Background" Value="Transparent"></Setter>
|
<Setter Property="Background" Value="Transparent" />
|
||||||
<Setter Property="BorderBrush" Value="LightGray"></Setter>
|
<Setter Property="BorderBrush" Value="LightGray" />
|
||||||
<Setter Property="CornerRadius" Value="5, 0, 0, 5"></Setter>
|
<Setter Property="CornerRadius" Value="5, 0, 0, 5" />
|
||||||
</Style>
|
</Style>
|
||||||
</UserControl.Styles>
|
</UserControl.Styles>
|
||||||
|
|
||||||
<Grid ColumnDefinitions="*, 30" Height="30">
|
<Grid ColumnDefinitions="*, 30" Height="30">
|
||||||
<TextBox Grid.Column="0" Name="Path" Height="30" x:Name="TextBox" IsEnabled="False"></TextBox>
|
<TextBox Grid.Column="0" Name="Path" Height="30" x:Name="TextBox" IsEnabled="False" />
|
||||||
<Button Grid.Column="1" Name="SelectButton" Height="30">
|
<Button Grid.Column="1" Name="SelectButton" Height="30">
|
||||||
<i:MaterialIcon Kind="Search"></i:MaterialIcon>
|
<i:MaterialIcon Kind="Search" />
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
@ -1,73 +1,70 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reactive;
|
|
||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using Wabbajack.Paths;
|
using Wabbajack.Paths;
|
||||||
|
|
||||||
namespace Wabbajack.App.Controls
|
namespace Wabbajack.App.Controls;
|
||||||
|
|
||||||
|
public partial class FileSelectionBox : ReactiveUserControl<FileSelectionBoxViewModel>
|
||||||
{
|
{
|
||||||
public partial class FileSelectionBox : ReactiveUserControl<FileSelectionBoxViewModel>
|
public static readonly DirectProperty<FileSelectionBox, AbsolutePath> SelectedPathProperty =
|
||||||
|
AvaloniaProperty.RegisterDirect<FileSelectionBox, AbsolutePath>(nameof(SelectedPath), o => o.SelectedPath);
|
||||||
|
|
||||||
|
public static readonly StyledProperty<string> AllowedExtensionsProperty =
|
||||||
|
AvaloniaProperty.Register<FileSelectionBox, string>(nameof(AllowedExtensions));
|
||||||
|
|
||||||
|
public static readonly StyledProperty<bool> SelectFolderProperty =
|
||||||
|
AvaloniaProperty.Register<FileSelectionBox, bool>(nameof(SelectFolder));
|
||||||
|
|
||||||
|
private AbsolutePath _selectedPath;
|
||||||
|
|
||||||
|
public FileSelectionBox()
|
||||||
{
|
{
|
||||||
public FileSelectionBox()
|
DataContext = App.Services.GetService<FileSelectionBoxViewModel>()!;
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
this.WhenActivated(disposables =>
|
||||||
{
|
{
|
||||||
DataContext = App.Services.GetService<FileSelectionBoxViewModel>()!;
|
this.Bind(ViewModel, vm => vm.Path, view => view.SelectedPath)
|
||||||
InitializeComponent();
|
.DisposeWith(disposables);
|
||||||
|
this.WhenAnyValue(view => view.SelectFolder)
|
||||||
this.WhenActivated(disposables =>
|
.BindTo(ViewModel, vm => vm.SelectFolder)
|
||||||
{
|
.DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.Path, view => view.SelectedPath)
|
this.WhenAnyValue(view => view.AllowedExtensions)
|
||||||
.DisposeWith(disposables);
|
.Where(exts => !string.IsNullOrWhiteSpace(exts))
|
||||||
this.WhenAnyValue(view => view.SelectFolder)
|
.Select(exts =>
|
||||||
.BindTo(ViewModel, vm => vm.SelectFolder)
|
exts.Split("|", StringSplitOptions.RemoveEmptyEntries).Select(s => new Extension(s)).ToArray())
|
||||||
.DisposeWith(disposables);
|
.BindTo(ViewModel, vm => vm.Extensions)
|
||||||
this.WhenAnyValue(view => view.AllowedExtensions)
|
.DisposeWith(disposables);
|
||||||
.Where(exts => !string.IsNullOrWhiteSpace(exts))
|
this.Bind(ViewModel, vm => vm.Path,
|
||||||
.Select(exts =>
|
|
||||||
exts.Split("|", StringSplitOptions.RemoveEmptyEntries).Select(s => new Extension(s)).ToArray())
|
|
||||||
.BindTo(ViewModel, vm => vm.Extensions)
|
|
||||||
.DisposeWith(disposables);
|
|
||||||
this.Bind(ViewModel, vm => vm.Path,
|
|
||||||
view => view.TextBox.Text)
|
view => view.TextBox.Text)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
this.BindCommand(ViewModel, vm => vm.BrowseCommand,
|
this.BindCommand(ViewModel, vm => vm.BrowseCommand,
|
||||||
view => view.SelectButton)
|
view => view.SelectButton)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly DirectProperty<FileSelectionBox, AbsolutePath> SelectedPathProperty =
|
public AbsolutePath SelectedPath
|
||||||
AvaloniaProperty.RegisterDirect<FileSelectionBox, AbsolutePath>(nameof(SelectedPath), o => o.SelectedPath);
|
{
|
||||||
|
get => _selectedPath;
|
||||||
|
set => SetAndRaise(SelectedPathProperty, ref _selectedPath, value);
|
||||||
|
}
|
||||||
|
|
||||||
private AbsolutePath _selectedPath;
|
public string AllowedExtensions
|
||||||
public AbsolutePath SelectedPath
|
{
|
||||||
{
|
get => GetValue(AllowedExtensionsProperty);
|
||||||
get => _selectedPath;
|
set => SetValue(AllowedExtensionsProperty, value);
|
||||||
set => SetAndRaise(SelectedPathProperty, ref _selectedPath, value);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public static readonly StyledProperty<string> AllowedExtensionsProperty =
|
public bool SelectFolder
|
||||||
AvaloniaProperty.Register<FileSelectionBox, string>(nameof(AllowedExtensions));
|
{
|
||||||
public string AllowedExtensions
|
get => GetValue(SelectFolderProperty);
|
||||||
{
|
set => SetValue(SelectFolderProperty, value);
|
||||||
get => GetValue(AllowedExtensionsProperty);
|
|
||||||
set => SetValue(AllowedExtensionsProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static readonly StyledProperty<bool> SelectFolderProperty =
|
|
||||||
AvaloniaProperty.Register<FileSelectionBox, bool>(nameof(SelectFolder));
|
|
||||||
|
|
||||||
public bool SelectFolder
|
|
||||||
{
|
|
||||||
get => GetValue(SelectFolderProperty);
|
|
||||||
set => SetValue(SelectFolderProperty, value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reactive;
|
using System.Reactive;
|
||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
@ -9,57 +10,52 @@ using ReactiveUI.Fody.Helpers;
|
|||||||
using Wabbajack.App.ViewModels;
|
using Wabbajack.App.ViewModels;
|
||||||
using Wabbajack.Paths;
|
using Wabbajack.Paths;
|
||||||
|
|
||||||
namespace Wabbajack.App.Controls
|
namespace Wabbajack.App.Controls;
|
||||||
|
|
||||||
|
public class FileSelectionBoxViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
public class FileSelectionBoxViewModel : ViewModelBase
|
public FileSelectionBoxViewModel()
|
||||||
{
|
{
|
||||||
[Reactive] public AbsolutePath Path { get; set; }
|
Activator = new ViewModelActivator();
|
||||||
|
this.WhenActivated(disposables =>
|
||||||
[Reactive] public Extension[] Extensions { get; set; } = Array.Empty<Extension>();
|
|
||||||
|
|
||||||
[Reactive] public bool SelectFolder { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public ReactiveCommand<Unit, Task> BrowseCommand { get; set; } = null!;
|
|
||||||
|
|
||||||
public FileSelectionBoxViewModel()
|
|
||||||
{
|
{
|
||||||
|
BrowseCommand = ReactiveCommand.Create(async () =>
|
||||||
Activator = new ViewModelActivator();
|
|
||||||
this.WhenActivated(disposables =>
|
|
||||||
{
|
{
|
||||||
BrowseCommand = ReactiveCommand.Create(async () =>
|
if (SelectFolder)
|
||||||
{
|
{
|
||||||
if (SelectFolder)
|
var dialog = new OpenFolderDialog
|
||||||
{
|
{
|
||||||
var dialog = new OpenFolderDialog()
|
Title = "Select a folder"
|
||||||
{
|
};
|
||||||
Title = "Select a folder",
|
var result = await dialog.ShowAsync(App.MainWindow);
|
||||||
};
|
if (result != null)
|
||||||
var result = await dialog.ShowAsync(App.MainWindow);
|
Path = result.ToAbsolutePath();
|
||||||
if (result != null)
|
}
|
||||||
Path = result.ToAbsolutePath();
|
else
|
||||||
}
|
{
|
||||||
else
|
var extensions = Extensions.Select(e => e.ToString()[1..]).ToList();
|
||||||
|
var dialog = new OpenFileDialog
|
||||||
{
|
{
|
||||||
var extensions = Extensions.Select(e => e.ToString()[1..]).ToList();
|
AllowMultiple = false,
|
||||||
var dialog = new OpenFileDialog
|
Title = "Select a file",
|
||||||
|
Filters = new List<FileDialogFilter>
|
||||||
{
|
{
|
||||||
AllowMultiple = false,
|
new FileDialogFilter {Extensions = extensions, Name = "*"}
|
||||||
Title = "Select a file",
|
}
|
||||||
Filters = new()
|
};
|
||||||
{
|
var results = await dialog.ShowAsync(App.MainWindow);
|
||||||
new FileDialogFilter { Extensions = extensions, Name = "*" }
|
if (results != null)
|
||||||
}
|
Path = results!.First().ToAbsolutePath();
|
||||||
};
|
}
|
||||||
var results = await dialog.ShowAsync(App.MainWindow);
|
}).DisposeWith(disposables);
|
||||||
if (results != null)
|
});
|
||||||
Path = results!.First().ToAbsolutePath();
|
|
||||||
}
|
|
||||||
}).DisposeWith(disposables);
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Reactive] public AbsolutePath Path { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public Extension[] Extensions { get; set; } = Array.Empty<Extension>();
|
||||||
|
|
||||||
|
[Reactive] public bool SelectFolder { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public ReactiveCommand<Unit, Task> BrowseCommand { get; set; } = null!;
|
||||||
}
|
}
|
@ -4,5 +4,5 @@
|
|||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Wabbajack.App.Controls.GameSelectorItemView">
|
x:Class="Wabbajack.App.Controls.GameSelectorItemView">
|
||||||
<TextBlock x:Name="GameName"></TextBlock>
|
<TextBlock x:Name="GameName" />
|
||||||
</UserControl>
|
</UserControl>
|
@ -1,24 +1,19 @@
|
|||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
namespace Wabbajack.App.Controls
|
namespace Wabbajack.App.Controls;
|
||||||
|
|
||||||
|
public partial class GameSelectorItemView : ReactiveUserControl<GameSelectorItemViewModel>
|
||||||
{
|
{
|
||||||
public partial class GameSelectorItemView : ReactiveUserControl<GameSelectorItemViewModel>
|
public GameSelectorItemView()
|
||||||
{
|
{
|
||||||
public GameSelectorItemView()
|
InitializeComponent();
|
||||||
|
|
||||||
|
this.WhenActivated(disposables =>
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
this.OneWayBind(ViewModel, vm => vm.Name, view => view.GameName.Text)
|
||||||
|
.DisposeWith(disposables);
|
||||||
this.WhenActivated(disposables =>
|
});
|
||||||
{
|
|
||||||
this.OneWayBind(ViewModel, vm => vm.Name, view => view.GameName.Text)
|
|
||||||
.DisposeWith(disposables);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,21 +3,18 @@ using ReactiveUI.Fody.Helpers;
|
|||||||
using Wabbajack.App.ViewModels;
|
using Wabbajack.App.ViewModels;
|
||||||
using Wabbajack.DTOs;
|
using Wabbajack.DTOs;
|
||||||
|
|
||||||
namespace Wabbajack.App.Controls
|
namespace Wabbajack.App.Controls;
|
||||||
|
|
||||||
|
public class GameSelectorItemViewModel : ViewModelBase, IActivatableViewModel
|
||||||
{
|
{
|
||||||
public class GameSelectorItemViewModel : ViewModelBase, IActivatableViewModel
|
public GameSelectorItemViewModel(Game game)
|
||||||
{
|
{
|
||||||
[Reactive]
|
Activator = new ViewModelActivator();
|
||||||
public Game Game { get; set; }
|
Game = game;
|
||||||
|
Name = game.MetaData().HumanFriendlyGameName;
|
||||||
[Reactive]
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
public GameSelectorItemViewModel(Game game)
|
|
||||||
{
|
|
||||||
Activator = new ViewModelActivator();
|
|
||||||
Game = game;
|
|
||||||
Name = game.MetaData().HumanFriendlyGameName;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Reactive] public Game Game { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public string Name { get; set; }
|
||||||
}
|
}
|
@ -6,10 +6,10 @@
|
|||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Wabbajack.App.Controls.InstalledListView">
|
x:Class="Wabbajack.App.Controls.InstalledListView">
|
||||||
<Grid RowDefinitions="Auto, Auto" ColumnDefinitions="*, Auto">
|
<Grid RowDefinitions="Auto, Auto" ColumnDefinitions="*, Auto">
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0" x:Name="Title"></TextBlock>
|
<TextBlock Grid.Row="0" Grid.Column="0" x:Name="Title" />
|
||||||
<TextBlock Grid.Row="1" Grid.Column="0" x:Name="InstallationPath"></TextBlock>
|
<TextBlock Grid.Row="1" Grid.Column="0" x:Name="InstallationPath" />
|
||||||
<Button Grid.Row="0" Grid.Column="1" Grid.RowSpan="2" x:Name="PlayButton">
|
<Button Grid.Row="0" Grid.Column="1" Grid.RowSpan="2" x:Name="PlayButton">
|
||||||
<i:MaterialIcon Kind="PlayArrow"></i:MaterialIcon>
|
<i:MaterialIcon Kind="PlayArrow" />
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
@ -1,7 +1,6 @@
|
|||||||
using Avalonia.Controls.Mixins;
|
using Avalonia.Controls.Mixins;
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using Wabbajack.App.Utilities;
|
|
||||||
|
|
||||||
namespace Wabbajack.App.Controls;
|
namespace Wabbajack.App.Controls;
|
||||||
|
|
||||||
@ -15,14 +14,13 @@ public partial class InstalledListView : ReactiveUserControl<InstalledListViewMo
|
|||||||
{
|
{
|
||||||
this.OneWayBind(ViewModel, vm => vm.Name, view => view.Title.Text)
|
this.OneWayBind(ViewModel, vm => vm.Name, view => view.Title.Text)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.OneWayBind(ViewModel, vm => vm.InstallPath, view => view.Title.Text,
|
this.OneWayBind(ViewModel, vm => vm.InstallPath, view => view.Title.Text,
|
||||||
p => p.ToString())
|
p => p.ToString())
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.BindCommand(ViewModel, vm => vm.Play, view => view.PlayButton)
|
this.BindCommand(ViewModel, vm => vm.Play, view => view.PlayButton)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,6 +1,5 @@
|
|||||||
using System.Reactive;
|
using System.Reactive;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using ReactiveUI.Fody.Helpers;
|
|
||||||
using Wabbajack.App.Messages;
|
using Wabbajack.App.Messages;
|
||||||
using Wabbajack.App.Screens;
|
using Wabbajack.App.Screens;
|
||||||
using Wabbajack.App.ViewModels;
|
using Wabbajack.App.ViewModels;
|
||||||
@ -12,10 +11,6 @@ namespace Wabbajack.App.Controls;
|
|||||||
public class InstalledListViewModel : ViewModelBase
|
public class InstalledListViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
private readonly InstallationConfigurationSetting _setting;
|
private readonly InstallationConfigurationSetting _setting;
|
||||||
public AbsolutePath InstallPath => _setting.Install;
|
|
||||||
|
|
||||||
public string Name => _setting.Metadata?.Title ?? "";
|
|
||||||
public ReactiveCommand<Unit, Unit> Play { get; }
|
|
||||||
|
|
||||||
public InstalledListViewModel(InstallationConfigurationSetting setting)
|
public InstalledListViewModel(InstallationConfigurationSetting setting)
|
||||||
{
|
{
|
||||||
@ -28,5 +23,9 @@ public class InstalledListViewModel : ViewModelBase
|
|||||||
MessageBus.Instance.Send(new NavigateTo(typeof(LauncherViewModel)));
|
MessageBus.Instance.Send(new NavigateTo(typeof(LauncherViewModel)));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AbsolutePath InstallPath => _setting.Install;
|
||||||
|
|
||||||
|
public string Name => _setting.Metadata?.Title ?? "";
|
||||||
|
public ReactiveCommand<Unit, Unit> Play { get; }
|
||||||
}
|
}
|
@ -6,10 +6,10 @@
|
|||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Wabbajack.App.Controls.LargeIconButton">
|
x:Class="Wabbajack.App.Controls.LargeIconButton">
|
||||||
<Button x:Name="Button">
|
<Button x:Name="Button">
|
||||||
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
|
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
<i:MaterialIcon x:Name="IconControl" Width="140" Height="140"></i:MaterialIcon>
|
<i:MaterialIcon x:Name="IconControl" Width="140" Height="140" />
|
||||||
<TextBlock x:Name="TextBlock" HorizontalAlignment="Center" FontSize="28" FontWeight="Bold"></TextBlock>
|
<TextBlock x:Name="TextBlock" HorizontalAlignment="Center" FontSize="28" FontWeight="Bold" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
</UserControl>
|
</UserControl>
|
@ -2,52 +2,46 @@ using System.Reactive.Disposables;
|
|||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Material.Icons;
|
using Material.Icons;
|
||||||
using Material.Icons.Avalonia;
|
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
namespace Wabbajack.App.Controls
|
namespace Wabbajack.App.Controls;
|
||||||
|
|
||||||
|
public partial class LargeIconButton : UserControl, IActivatableView
|
||||||
{
|
{
|
||||||
public partial class LargeIconButton : UserControl, IActivatableView
|
public static readonly StyledProperty<string> TextProperty =
|
||||||
|
AvaloniaProperty.Register<LargeIconButton, string>(nameof(Text));
|
||||||
|
|
||||||
|
|
||||||
|
public static readonly StyledProperty<MaterialIconKind> IconProperty =
|
||||||
|
AvaloniaProperty.Register<LargeIconButton, MaterialIconKind>(nameof(IconProperty));
|
||||||
|
|
||||||
|
public LargeIconButton()
|
||||||
{
|
{
|
||||||
public static readonly StyledProperty<string> TextProperty =
|
InitializeComponent();
|
||||||
AvaloniaProperty.Register<LargeIconButton, string>(nameof(Text));
|
this.WhenActivated(dispose =>
|
||||||
|
|
||||||
public string Text
|
|
||||||
{
|
{
|
||||||
get => GetValue(TextProperty);
|
this.WhenAnyValue(x => x.Icon)
|
||||||
set => SetValue(TextProperty, value);
|
.Where(x => x != default)
|
||||||
}
|
.BindTo(IconControl, x => x.Kind)
|
||||||
|
.DisposeWith(dispose);
|
||||||
|
|
||||||
|
this.WhenAnyValue(x => x.Text)
|
||||||
public static readonly StyledProperty<MaterialIconKind> IconProperty =
|
.Where(x => x != default)
|
||||||
AvaloniaProperty.Register<LargeIconButton, MaterialIconKind>(nameof(IconProperty));
|
.BindTo(TextBlock, x => x.Text)
|
||||||
|
.DisposeWith(dispose);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public MaterialIconKind Icon
|
public string Text
|
||||||
{
|
{
|
||||||
get => GetValue(IconProperty);
|
get => GetValue(TextProperty);
|
||||||
set => SetValue(IconProperty, value);
|
set => SetValue(TextProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public LargeIconButton()
|
public MaterialIconKind Icon
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
get => GetValue(IconProperty);
|
||||||
this.WhenActivated(dispose =>
|
set => SetValue(IconProperty, value);
|
||||||
{
|
|
||||||
this.WhenAnyValue(x => x.Icon)
|
|
||||||
.Where(x => x != default)
|
|
||||||
.BindTo(IconControl, x => x.Kind)
|
|
||||||
.DisposeWith(dispose);
|
|
||||||
|
|
||||||
this.WhenAnyValue(x => x.Text)
|
|
||||||
.Where(x => x != default)
|
|
||||||
.BindTo(TextBlock, x => x.Text)
|
|
||||||
.DisposeWith(dispose);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -13,22 +13,22 @@
|
|||||||
<ItemsControl x:Name="Messages">
|
<ItemsControl x:Name="Messages">
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsControl.ItemsPanel>
|
||||||
<ItemsPanelTemplate>
|
<ItemsPanelTemplate>
|
||||||
<StackPanel></StackPanel>
|
<StackPanel />
|
||||||
</ItemsPanelTemplate>
|
</ItemsPanelTemplate>
|
||||||
</ItemsControl.ItemsPanel>
|
</ItemsControl.ItemsPanel>
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<controls:LogViewItem></controls:LogViewItem>
|
<controls:LogViewItem />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right">
|
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
<Button x:Name="CopyLog">
|
<Button x:Name="CopyLog">
|
||||||
<avalonia:MaterialIcon Kind="ContentCopy"></avalonia:MaterialIcon>
|
<avalonia:MaterialIcon Kind="ContentCopy" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button x:Name="OpenFolder">
|
<Button x:Name="OpenFolder">
|
||||||
<avalonia:MaterialIcon Kind="Folder"></avalonia:MaterialIcon>
|
<avalonia:MaterialIcon Kind="Folder" />
|
||||||
</Button>
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -3,7 +3,6 @@ using Avalonia.Controls.Mixins;
|
|||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using Wabbajack.App.Utilities;
|
|
||||||
|
|
||||||
namespace Wabbajack.App.Controls;
|
namespace Wabbajack.App.Controls;
|
||||||
|
|
||||||
|
@ -4,5 +4,5 @@
|
|||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Wabbajack.App.Controls.LogViewItem">
|
x:Class="Wabbajack.App.Controls.LogViewItem">
|
||||||
<TextBlock x:Name="Message" FontSize="10"></TextBlock>
|
<TextBlock x:Name="Message" FontSize="10" />
|
||||||
</UserControl>
|
</UserControl>
|
@ -16,5 +16,4 @@ public partial class LogViewItem : ReactiveUserControl<LoggerProvider.ILogMessag
|
|||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,13 +1,8 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Reactive;
|
using System.Reactive;
|
||||||
using System.Reactive.Disposables;
|
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls.Mixins;
|
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using DynamicData;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using ReactiveUI.Fody.Helpers;
|
using ReactiveUI.Fody.Helpers;
|
||||||
using Wabbajack.App.Utilities;
|
using Wabbajack.App.Utilities;
|
||||||
@ -18,16 +13,12 @@ namespace Wabbajack.App.Controls;
|
|||||||
public class LogViewModel : ViewModelBase, IActivatableViewModel
|
public class LogViewModel : ViewModelBase, IActivatableViewModel
|
||||||
{
|
{
|
||||||
private readonly LoggerProvider _provider;
|
private readonly LoggerProvider _provider;
|
||||||
public ReadOnlyObservableCollection<LoggerProvider.ILogMessage> Messages => _provider.MessageLog;
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public ReactiveCommand<Unit, Unit> CopyLogFile { get; set; }
|
|
||||||
|
|
||||||
public LogViewModel(LoggerProvider provider)
|
public LogViewModel(LoggerProvider provider)
|
||||||
{
|
{
|
||||||
Activator = new ViewModelActivator();
|
Activator = new ViewModelActivator();
|
||||||
_provider = provider;
|
_provider = provider;
|
||||||
|
|
||||||
CopyLogFile = ReactiveCommand.Create(() =>
|
CopyLogFile = ReactiveCommand.Create(() =>
|
||||||
{
|
{
|
||||||
var obj = new DataObject();
|
var obj = new DataObject();
|
||||||
@ -35,4 +26,8 @@ public class LogViewModel : ViewModelBase, IActivatableViewModel
|
|||||||
Application.Current.Clipboard.SetDataObjectAsync(obj);
|
Application.Current.Clipboard.SetDataObjectAsync(obj);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ReadOnlyObservableCollection<LoggerProvider.ILogMessage> Messages => _provider.MessageLog;
|
||||||
|
|
||||||
|
[Reactive] public ReactiveCommand<Unit, Unit> CopyLogFile { get; set; }
|
||||||
}
|
}
|
@ -7,8 +7,8 @@
|
|||||||
x:Class="Wabbajack.App.Controls.RemovableListItem">
|
x:Class="Wabbajack.App.Controls.RemovableListItem">
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<Button x:Name="DeleteButton">
|
<Button x:Name="DeleteButton">
|
||||||
<i:MaterialIcon Kind="MinusCircle"></i:MaterialIcon>
|
<i:MaterialIcon Kind="MinusCircle" />
|
||||||
</Button>
|
</Button>
|
||||||
<TextBlock x:Name="Text" VerticalAlignment="Center"></TextBlock>
|
<TextBlock x:Name="Text" VerticalAlignment="Center" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</UserControl>
|
</UserControl>
|
@ -1,7 +1,6 @@
|
|||||||
using Avalonia.Controls.Mixins;
|
using Avalonia.Controls.Mixins;
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using Wabbajack.App.ViewModels;
|
|
||||||
|
|
||||||
namespace Wabbajack.App.Controls;
|
namespace Wabbajack.App.Controls;
|
||||||
|
|
||||||
@ -17,8 +16,6 @@ public partial class RemovableListItem : ReactiveUserControl<RemovableItemViewMo
|
|||||||
|
|
||||||
this.BindCommand(ViewModel, vm => vm.DeleteCommand, view => view.DeleteButton)
|
this.BindCommand(ViewModel, vm => vm.DeleteCommand, view => view.DeleteButton)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,4 +1,3 @@
|
|||||||
using System;
|
|
||||||
using System.Reactive;
|
using System.Reactive;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using ReactiveUI.Fody.Helpers;
|
using ReactiveUI.Fody.Helpers;
|
||||||
@ -8,15 +7,12 @@ namespace Wabbajack.App.Controls;
|
|||||||
|
|
||||||
public class RemovableItemViewModel : ViewModelBase
|
public class RemovableItemViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
[Reactive]
|
|
||||||
public string Text { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public ReactiveCommand<Unit, Unit> DeleteCommand { get; set; }
|
|
||||||
|
|
||||||
public RemovableItemViewModel()
|
public RemovableItemViewModel()
|
||||||
{
|
{
|
||||||
Activator = new ViewModelActivator();
|
Activator = new ViewModelActivator();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Reactive] public string Text { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public ReactiveCommand<Unit, Unit> DeleteCommand { get; set; }
|
||||||
}
|
}
|
@ -5,12 +5,12 @@
|
|||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Wabbajack.App.Controls.ResourceView">
|
x:Class="Wabbajack.App.Controls.ResourceView">
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<TextBlock x:Name="ResourceName" Width="100" HorizontalAlignment="Left" VerticalAlignment="Center"></TextBlock>
|
<TextBlock x:Name="ResourceName" Width="100" HorizontalAlignment="Left" VerticalAlignment="Center" />
|
||||||
<Label Width="100" HorizontalContentAlignment="Right" VerticalAlignment="Center">Tasks:</Label>
|
<Label Width="100" HorizontalContentAlignment="Right" VerticalAlignment="Center">Tasks:</Label>
|
||||||
<TextBox x:Name="MaxTasks" Width="20" HorizontalAlignment="Left" VerticalAlignment="Center"></TextBox>
|
<TextBox x:Name="MaxTasks" Width="20" HorizontalAlignment="Left" VerticalAlignment="Center" />
|
||||||
<Label Width="100" HorizontalContentAlignment="Right" VerticalAlignment="Center">Throughput:</Label>
|
<Label Width="100" HorizontalContentAlignment="Right" VerticalAlignment="Center">Throughput:</Label>
|
||||||
<TextBox x:Name="MaxThroughput" Width="20" HorizontalAlignment="Left" VerticalAlignment="Center"></TextBox>
|
<TextBox x:Name="MaxThroughput" Width="20" HorizontalAlignment="Left" VerticalAlignment="Center" />
|
||||||
<Label Width="100" HorizontalContentAlignment="Right" VerticalAlignment="Center">Status:</Label>
|
<Label Width="100" HorizontalContentAlignment="Right" VerticalAlignment="Center">Status:</Label>
|
||||||
<TextBlock x:Name="CurrentThrougput" Width="50" HorizontalAlignment="Left" VerticalAlignment="Center"></TextBlock>
|
<TextBlock x:Name="CurrentThrougput" Width="50" HorizontalAlignment="Left" VerticalAlignment="Center" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</UserControl>
|
</UserControl>
|
@ -1,5 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
using FluentFTP.Helpers;
|
using FluentFTP.Helpers;
|
||||||
@ -16,17 +14,15 @@ public partial class ResourceView : ReactiveUserControl<ResourceViewModel>, IAct
|
|||||||
{
|
{
|
||||||
this.OneWayBind(ViewModel, vm => vm.Name, view => view.ResourceName.Text)
|
this.OneWayBind(ViewModel, vm => vm.Name, view => view.ResourceName.Text)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.Bind(ViewModel, vm => vm.MaxTasks, view => view.MaxTasks.Text)
|
this.Bind(ViewModel, vm => vm.MaxTasks, view => view.MaxTasks.Text)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.MaxThroughput, view => view.MaxThroughput.Text)
|
this.Bind(ViewModel, vm => vm.MaxThroughput, view => view.MaxThroughput.Text)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.OneWayBind(ViewModel, vm => vm.CurrentThroughput, view => view.CurrentThrougput.Text,
|
this.OneWayBind(ViewModel, vm => vm.CurrentThroughput, view => view.CurrentThrougput.Text,
|
||||||
val => val.FileSizeToString())
|
val => val.FileSizeToString())
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,7 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
using System.Timers;using ReactiveUI;
|
using System.Timers;
|
||||||
|
using ReactiveUI;
|
||||||
using ReactiveUI.Fody.Helpers;
|
using ReactiveUI.Fody.Helpers;
|
||||||
using Wabbajack.App.ViewModels;
|
using Wabbajack.App.ViewModels;
|
||||||
using Wabbajack.RateLimiter;
|
using Wabbajack.RateLimiter;
|
||||||
@ -12,18 +13,6 @@ public class ResourceViewModel : ViewModelBase, IActivatableViewModel, IDisposab
|
|||||||
{
|
{
|
||||||
private readonly IResource _resource;
|
private readonly IResource _resource;
|
||||||
private readonly Timer _timer;
|
private readonly Timer _timer;
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public int MaxTasks { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public long MaxThroughput { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public long CurrentThroughput { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
public ResourceViewModel(IResource resource)
|
public ResourceViewModel(IResource resource)
|
||||||
{
|
{
|
||||||
@ -32,12 +21,12 @@ public class ResourceViewModel : ViewModelBase, IActivatableViewModel, IDisposab
|
|||||||
_timer = new Timer(1.0);
|
_timer = new Timer(1.0);
|
||||||
|
|
||||||
Name = resource.Name;
|
Name = resource.Name;
|
||||||
|
|
||||||
this.WhenActivated(disposables =>
|
this.WhenActivated(disposables =>
|
||||||
{
|
{
|
||||||
_timer.Elapsed += TimerElapsed;
|
_timer.Elapsed += TimerElapsed;
|
||||||
_timer.Start();
|
_timer.Start();
|
||||||
|
|
||||||
Disposable.Create(() =>
|
Disposable.Create(() =>
|
||||||
{
|
{
|
||||||
_timer.Stop();
|
_timer.Stop();
|
||||||
@ -46,31 +35,32 @@ public class ResourceViewModel : ViewModelBase, IActivatableViewModel, IDisposab
|
|||||||
|
|
||||||
this.WhenAnyValue(vm => vm.MaxThroughput)
|
this.WhenAnyValue(vm => vm.MaxThroughput)
|
||||||
.Skip(1)
|
.Skip(1)
|
||||||
.Subscribe(v =>
|
.Subscribe(v => { _resource.MaxThroughput = MaxThroughput; }).DisposeWith(disposables);
|
||||||
{
|
|
||||||
_resource.MaxThroughput = MaxThroughput;
|
|
||||||
}).DisposeWith(disposables);
|
|
||||||
|
|
||||||
this.WhenAnyValue(vm => vm.MaxTasks)
|
this.WhenAnyValue(vm => vm.MaxTasks)
|
||||||
.Skip(1)
|
.Skip(1)
|
||||||
.Subscribe(v =>
|
.Subscribe(v => { _resource.MaxTasks = MaxTasks; }).DisposeWith(disposables);
|
||||||
{
|
|
||||||
_resource.MaxTasks = MaxTasks;
|
|
||||||
}).DisposeWith(disposables);
|
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Reactive] public int MaxTasks { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public long MaxThroughput { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public long CurrentThroughput { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public string Name { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_timer.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
private void TimerElapsed(object? sender, ElapsedEventArgs e)
|
private void TimerElapsed(object? sender, ElapsedEventArgs e)
|
||||||
{
|
{
|
||||||
MaxTasks = _resource.MaxTasks;
|
MaxTasks = _resource.MaxTasks;
|
||||||
MaxThroughput = _resource.MaxThroughput;
|
MaxThroughput = _resource.MaxThroughput;
|
||||||
CurrentThroughput = _resource.StatusReport.Transferred;
|
CurrentThroughput = _resource.StatusReport.Transferred;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_timer.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -4,7 +4,7 @@
|
|||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Wabbajack.App.Controls.TagView">
|
x:Class="Wabbajack.App.Controls.TagView">
|
||||||
<Border x:Name = "Border"
|
<Border x:Name="Border"
|
||||||
Margin="5,5,0,5"
|
Margin="5,5,0,5"
|
||||||
BorderThickness="1"
|
BorderThickness="1"
|
||||||
CornerRadius="7,7,7,7"
|
CornerRadius="7,7,7,7"
|
||||||
@ -13,6 +13,6 @@
|
|||||||
<TextBlock
|
<TextBlock
|
||||||
x:Name="Text"
|
x:Name="Text"
|
||||||
Margin="5,5,5,5"
|
Margin="5,5,5,5"
|
||||||
FontSize="10"/>
|
FontSize="10" />
|
||||||
</Border>
|
</Border>
|
||||||
</UserControl>
|
</UserControl>
|
@ -1,26 +1,22 @@
|
|||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
using DynamicData;
|
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
namespace Wabbajack.App.Controls
|
namespace Wabbajack.App.Controls;
|
||||||
|
|
||||||
|
public partial class TagView : ReactiveUserControl<TagViewModel>
|
||||||
{
|
{
|
||||||
public partial class TagView : ReactiveUserControl<TagViewModel>
|
public TagView()
|
||||||
{
|
{
|
||||||
public TagView()
|
InitializeComponent();
|
||||||
|
this.WhenActivated(disposables =>
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
this.OneWayBind(ViewModel, vm => vm.Name, view => view.Text.Text)
|
||||||
this.WhenActivated(disposables =>
|
.DisposeWith(disposables);
|
||||||
{
|
this.OneWayBind(ViewModel, vm => vm.Tag, view => view.Classes,
|
||||||
this.OneWayBind(ViewModel, vm => vm.Name, view => view.Text.Text)
|
|
||||||
.DisposeWith(disposables);
|
|
||||||
this.OneWayBind(ViewModel, vm => vm.Tag, view => view.Classes,
|
|
||||||
c => c == null ? new Classes() : new Classes(c))
|
c => c == null ? new Classes() : new Classes(c))
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,22 +2,18 @@ using ReactiveUI;
|
|||||||
using ReactiveUI.Fody.Helpers;
|
using ReactiveUI.Fody.Helpers;
|
||||||
using Wabbajack.App.ViewModels;
|
using Wabbajack.App.ViewModels;
|
||||||
|
|
||||||
namespace Wabbajack.App.Controls
|
namespace Wabbajack.App.Controls;
|
||||||
|
|
||||||
|
public class TagViewModel : ViewModelBase, IActivatableViewModel
|
||||||
{
|
{
|
||||||
public class TagViewModel : ViewModelBase, IActivatableViewModel
|
public TagViewModel(string name, string tag)
|
||||||
{
|
{
|
||||||
[Reactive]
|
Activator = new ViewModelActivator();
|
||||||
public string Name { get; set; }
|
Name = name;
|
||||||
|
Tag = tag;
|
||||||
[Reactive]
|
|
||||||
public string Tag { get; set; }
|
|
||||||
|
|
||||||
public TagViewModel(string name, string tag)
|
|
||||||
{
|
|
||||||
Activator = new ViewModelActivator();
|
|
||||||
Name = name;
|
|
||||||
Tag = tag;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Reactive] public string Name { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public string Tag { get; set; }
|
||||||
}
|
}
|
@ -2,32 +2,31 @@ using System;
|
|||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using Wabbajack.Paths;
|
using Wabbajack.Paths;
|
||||||
|
|
||||||
namespace Wabbajack.App.Converters
|
namespace Wabbajack.App.Converters;
|
||||||
{
|
|
||||||
public class AbsoultePathBindingConverter : IBindingTypeConverter
|
|
||||||
{
|
|
||||||
public int GetAffinityForObjects(Type fromType, Type toType)
|
|
||||||
{
|
|
||||||
if (fromType == typeof(string) && toType == typeof(AbsolutePath) ||
|
|
||||||
fromType == typeof(AbsolutePath) && toType == typeof(string))
|
|
||||||
return 100;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryConvert(object? @from, Type toType, object? conversionHint, out object? result)
|
public class AbsoultePathBindingConverter : IBindingTypeConverter
|
||||||
|
{
|
||||||
|
public int GetAffinityForObjects(Type fromType, Type toType)
|
||||||
|
{
|
||||||
|
if (fromType == typeof(string) && toType == typeof(AbsolutePath) ||
|
||||||
|
fromType == typeof(AbsolutePath) && toType == typeof(string))
|
||||||
|
return 100;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryConvert(object? from, Type toType, object? conversionHint, out object? result)
|
||||||
|
{
|
||||||
|
switch (from)
|
||||||
{
|
{
|
||||||
switch (@from)
|
case string s:
|
||||||
{
|
result = (AbsolutePath) s;
|
||||||
case string s:
|
return true;
|
||||||
result = (AbsolutePath)s;
|
case AbsolutePath ap:
|
||||||
return true;
|
result = ap.ToString();
|
||||||
case AbsolutePath ap:
|
return true;
|
||||||
result = ap.ToString();
|
default:
|
||||||
return true;
|
result = null;
|
||||||
default:
|
return false;
|
||||||
result = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,24 +3,22 @@ using System.Reactive.Disposables;
|
|||||||
using System.Reactive.Subjects;
|
using System.Reactive.Subjects;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Wabbajack.App.Extensions
|
namespace Wabbajack.App.Extensions;
|
||||||
|
|
||||||
|
public static class IObservableExtensions
|
||||||
{
|
{
|
||||||
public static class IObservableExtensions
|
public static IObservable<TOut> SelectAsync<TIn, TOut>(this IObservable<TIn> input,
|
||||||
|
CompositeDisposable disposable,
|
||||||
|
Func<TIn, ValueTask<TOut>> func)
|
||||||
{
|
{
|
||||||
public static IObservable<TOut> SelectAsync<TIn, TOut>(this IObservable<TIn> input,
|
Subject<TOut> returnObs = new();
|
||||||
CompositeDisposable disposable,
|
|
||||||
Func<TIn, ValueTask<TOut>> func)
|
input.Subscribe(x => Task.Run(async () =>
|
||||||
{
|
{
|
||||||
Subject<TOut> returnObs = new();
|
var result = await func(x);
|
||||||
|
returnObs.OnNext(result);
|
||||||
input.Subscribe(x => Task.Run(async () =>
|
})).DisposeWith(disposable);
|
||||||
{
|
|
||||||
var result = await func(x);
|
|
||||||
returnObs.OnNext(result);
|
|
||||||
})).DisposeWith(disposable);
|
|
||||||
|
|
||||||
return returnObs;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return returnObs;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,12 +1,5 @@
|
|||||||
using System;
|
namespace Wabbajack.App.Extensions;
|
||||||
using System.Linq.Expressions;
|
|
||||||
using System.Reactive.Linq;
|
|
||||||
using ReactiveUI;
|
|
||||||
|
|
||||||
namespace Wabbajack.App.Extensions
|
public static class ReactiveUIExtensions
|
||||||
{
|
{
|
||||||
public static class ReactiveUIExtensions
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -7,57 +7,49 @@ using CefNet.Avalonia;
|
|||||||
using HtmlAgilityPack;
|
using HtmlAgilityPack;
|
||||||
using Wabbajack.DTOs.Logins;
|
using Wabbajack.DTOs.Logins;
|
||||||
|
|
||||||
namespace Wabbajack.App.Extensions
|
namespace Wabbajack.App.Extensions;
|
||||||
|
|
||||||
|
public static class WebViewExtensions
|
||||||
{
|
{
|
||||||
public static class WebViewExtensions
|
public static async Task WaitForReady(this WebView view)
|
||||||
{
|
{
|
||||||
public static async Task WaitForReady(this WebView view)
|
while (view.BrowserObject == null) await Task.Delay(200);
|
||||||
{
|
}
|
||||||
while (view.BrowserObject == null)
|
|
||||||
{
|
|
||||||
await Task.Delay(200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Navigates to the URL and waits until the page is finished loading
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="view"></param>
|
|
||||||
/// <param name="uri"></param>
|
|
||||||
public static async Task NavigateTo(this WebView view, Uri uri)
|
|
||||||
{
|
|
||||||
view.Navigate(uri.ToString());
|
|
||||||
while (view.IsBusy)
|
|
||||||
{
|
|
||||||
await Task.Delay(200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<Cookie[]> Cookies(this WebView view, string domainEnding, CancellationToken token)
|
/// <summary>
|
||||||
{
|
/// Navigates to the URL and waits until the page is finished loading
|
||||||
var results = CefCookieManager.GetGlobalManager(null)!;
|
/// </summary>
|
||||||
var cookies = await results.GetCookiesAsync(c => c.Domain.EndsWith(domainEnding), token)!;
|
/// <param name="view"></param>
|
||||||
return cookies.Select(c => new Cookie
|
/// <param name="uri"></param>
|
||||||
{
|
public static async Task NavigateTo(this WebView view, Uri uri)
|
||||||
Domain = c.Domain,
|
{
|
||||||
Name = c.Name,
|
view.Navigate(uri.ToString());
|
||||||
Path = c.Path,
|
while (view.IsBusy) await Task.Delay(200);
|
||||||
Value = c.Value,
|
}
|
||||||
}).ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task EvaluateJavaScript(this WebView view, string js)
|
public static async Task<Cookie[]> Cookies(this WebView view, string domainEnding, CancellationToken token)
|
||||||
|
{
|
||||||
|
var results = CefCookieManager.GetGlobalManager(null)!;
|
||||||
|
var cookies = await results.GetCookiesAsync(c => c.Domain.EndsWith(domainEnding), token)!;
|
||||||
|
return cookies.Select(c => new Cookie
|
||||||
{
|
{
|
||||||
view.GetMainFrame().ExecuteJavaScript(js, "", 0);
|
Domain = c.Domain,
|
||||||
}
|
Name = c.Name,
|
||||||
|
Path = c.Path,
|
||||||
|
Value = c.Value
|
||||||
|
}).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
public static async Task<HtmlDocument> GetDom(this WebView view, CancellationToken token)
|
public static async Task EvaluateJavaScript(this WebView view, string js)
|
||||||
{
|
{
|
||||||
var source = await view.GetMainFrame().GetSourceAsync(token);
|
view.GetMainFrame().ExecuteJavaScript(js, "", 0);
|
||||||
var doc = new HtmlDocument();
|
}
|
||||||
doc.LoadHtml(source);
|
|
||||||
return doc;
|
public static async Task<HtmlDocument> GetDom(this WebView view, CancellationToken token)
|
||||||
}
|
{
|
||||||
|
var source = await view.GetMainFrame().GetSourceAsync(token);
|
||||||
|
var doc = new HtmlDocument();
|
||||||
|
doc.LoadHtml(source);
|
||||||
|
return doc;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,43 +5,42 @@ using Avalonia.Controls.Primitives;
|
|||||||
using Avalonia.Platform;
|
using Avalonia.Platform;
|
||||||
using Avalonia.Styling;
|
using Avalonia.Styling;
|
||||||
|
|
||||||
namespace Wabbajack.App
|
namespace Wabbajack.App;
|
||||||
|
|
||||||
|
public class FluentWindow : Window, IStyleable
|
||||||
{
|
{
|
||||||
public class FluentWindow : Window, IStyleable
|
public FluentWindow()
|
||||||
{
|
{
|
||||||
Type IStyleable.StyleKey => typeof(Window);
|
ExtendClientAreaToDecorationsHint = true;
|
||||||
|
ExtendClientAreaTitleBarHeightHint = -1;
|
||||||
|
|
||||||
public FluentWindow()
|
TransparencyLevelHint = WindowTransparencyLevel.AcrylicBlur;
|
||||||
{
|
|
||||||
ExtendClientAreaToDecorationsHint = true;
|
|
||||||
ExtendClientAreaTitleBarHeightHint = -1;
|
|
||||||
|
|
||||||
TransparencyLevelHint = WindowTransparencyLevel.AcrylicBlur;
|
this.GetObservable(WindowStateProperty)
|
||||||
|
.Subscribe(x =>
|
||||||
|
{
|
||||||
|
PseudoClasses.Set(":maximized", x == WindowState.Maximized);
|
||||||
|
PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen);
|
||||||
|
});
|
||||||
|
|
||||||
this.GetObservable(WindowStateProperty)
|
this.GetObservable(IsExtendedIntoWindowDecorationsProperty)
|
||||||
.Subscribe(x =>
|
.Subscribe(x =>
|
||||||
|
{
|
||||||
|
if (!x)
|
||||||
{
|
{
|
||||||
PseudoClasses.Set(":maximized", x == WindowState.Maximized);
|
SystemDecorations = SystemDecorations.Full;
|
||||||
PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen);
|
TransparencyLevelHint = WindowTransparencyLevel.Blur;
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.GetObservable(IsExtendedIntoWindowDecorationsProperty)
|
Type IStyleable.StyleKey => typeof(Window);
|
||||||
.Subscribe(x =>
|
|
||||||
{
|
|
||||||
if (!x)
|
|
||||||
{
|
|
||||||
SystemDecorations = SystemDecorations.Full;
|
|
||||||
TransparencyLevelHint = WindowTransparencyLevel.Blur;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||||
{
|
{
|
||||||
base.OnApplyTemplate(e);
|
base.OnApplyTemplate(e);
|
||||||
ExtendClientAreaChromeHints =
|
ExtendClientAreaChromeHints =
|
||||||
ExtendClientAreaChromeHints.PreferSystemChrome |
|
ExtendClientAreaChromeHints.PreferSystemChrome |
|
||||||
ExtendClientAreaChromeHints.OSXThickTitleBar;
|
ExtendClientAreaChromeHints.OSXThickTitleBar;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,3 +1,3 @@
|
|||||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||||
<ReactiveUI />
|
<ReactiveUI/>
|
||||||
</Weavers>
|
</Weavers>
|
@ -1,9 +1,8 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Wabbajack.App.Interfaces
|
namespace Wabbajack.App.Interfaces;
|
||||||
|
|
||||||
|
public interface INavigationParameter<T>
|
||||||
{
|
{
|
||||||
public interface INavigationParameter<T>
|
public Task NavigatedTo(T param);
|
||||||
{
|
|
||||||
public Task NavigatedTo(T param);
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,10 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Wabbajack.App.Interfaces
|
namespace Wabbajack.App.Interfaces;
|
||||||
|
|
||||||
|
public interface IScreenView
|
||||||
{
|
{
|
||||||
public interface IScreenView
|
public Type ViewModelType { get; }
|
||||||
{
|
|
||||||
public Type ViewModelType { get; }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -2,50 +2,49 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Wabbajack.App.Messages;
|
using Wabbajack.App.Messages;
|
||||||
|
|
||||||
namespace Wabbajack.App
|
namespace Wabbajack.App;
|
||||||
|
|
||||||
|
public interface IMessageBus
|
||||||
{
|
{
|
||||||
public interface IMessageBus
|
public void Send<T>(T message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MessageBus : IMessageBus
|
||||||
|
{
|
||||||
|
private readonly ILogger<MessageBus> _logger;
|
||||||
|
private readonly IReceiverMarker[] _receivers;
|
||||||
|
|
||||||
|
public MessageBus(ILogger<MessageBus> logger, IEnumerable<IReceiverMarker> receivers)
|
||||||
{
|
{
|
||||||
public void Send<T>(T message);
|
Instance = this;
|
||||||
|
_receivers = receivers.ToArray();
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MessageBus : IMessageBus
|
public static IMessageBus Instance { get; set; }
|
||||||
|
|
||||||
|
public void Send<T>(T message)
|
||||||
{
|
{
|
||||||
public static IMessageBus Instance { get; set; }
|
AvaloniaScheduler.Instance.Schedule(message, TimeSpan.FromMilliseconds(200), (_, msg) =>
|
||||||
private readonly IReceiverMarker[] _receivers;
|
|
||||||
private readonly ILogger<MessageBus> _logger;
|
|
||||||
|
|
||||||
public MessageBus(ILogger<MessageBus> logger, IEnumerable<IReceiverMarker> receivers)
|
|
||||||
{
|
{
|
||||||
Instance = this;
|
foreach (var receiver in _receivers.OfType<IReceiver<T>>())
|
||||||
_receivers = receivers.ToArray();
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Send<T>(T message)
|
|
||||||
{
|
|
||||||
AvaloniaScheduler.Instance.Schedule(message, TimeSpan.FromMilliseconds(200), (_, msg) =>
|
|
||||||
{
|
{
|
||||||
foreach (var receiver in _receivers.OfType<IReceiver<T>>())
|
_logger.LogInformation("Sending {msg} to {receiver}", msg, receiver);
|
||||||
|
try
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Sending {msg} to {receiver}", msg, receiver);
|
receiver.Receive(msg);
|
||||||
try
|
|
||||||
{
|
|
||||||
receiver.Receive(msg);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogCritical(ex, "Failed sending {msg} to {receiver}", msg, receiver);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogCritical(ex, "Failed sending {msg} to {receiver}", msg, receiver);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Disposable.Empty;
|
return Disposable.Empty;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,9 +1,7 @@
|
|||||||
using Wabbajack.Paths;
|
using Wabbajack.Paths;
|
||||||
|
|
||||||
namespace Wabbajack.App.Messages
|
namespace Wabbajack.App.Messages;
|
||||||
|
|
||||||
|
public record ConfigureLauncher(AbsolutePath InstallFolder)
|
||||||
{
|
{
|
||||||
public record ConfigureLauncher(AbsolutePath InstallFolder)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -4,5 +4,4 @@ namespace Wabbajack.App.Messages;
|
|||||||
|
|
||||||
public record Error(string Prefix, Exception Exception)
|
public record Error(string Prefix, Exception Exception)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
@ -1,10 +1,10 @@
|
|||||||
namespace Wabbajack.App.Messages
|
namespace Wabbajack.App.Messages;
|
||||||
|
|
||||||
|
public interface IReceiverMarker
|
||||||
{
|
{
|
||||||
public interface IReceiverMarker
|
}
|
||||||
{
|
|
||||||
}
|
public interface IReceiver<in T> : IReceiverMarker
|
||||||
public interface IReceiver<in T> : IReceiverMarker
|
{
|
||||||
{
|
public void Receive(T val);
|
||||||
public void Receive(T val);
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,10 +1,5 @@
|
|||||||
namespace Wabbajack.App.Messages
|
namespace Wabbajack.App.Messages;
|
||||||
|
|
||||||
|
public class NavigateBack
|
||||||
{
|
{
|
||||||
public class NavigateBack
|
|
||||||
{
|
|
||||||
public NavigateBack()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,10 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using Wabbajack.App.ViewModels;
|
|
||||||
|
|
||||||
namespace Wabbajack.App.Messages
|
namespace Wabbajack.App.Messages;
|
||||||
|
|
||||||
|
public record NavigateTo(Type ViewModel)
|
||||||
{
|
{
|
||||||
public record NavigateTo(Type ViewModel)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -4,5 +4,4 @@ namespace Wabbajack.App.Messages;
|
|||||||
|
|
||||||
public record StartCompilation(CompilerSettings Settings)
|
public record StartCompilation(CompilerSettings Settings)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
@ -1,8 +1,7 @@
|
|||||||
using Wabbajack.Paths;
|
using Wabbajack.Paths;
|
||||||
|
|
||||||
namespace Wabbajack.App.Messages
|
namespace Wabbajack.App.Messages;
|
||||||
|
|
||||||
|
public record StartInstallConfiguration(AbsolutePath ModList)
|
||||||
{
|
{
|
||||||
public record StartInstallConfiguration(AbsolutePath ModList)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,9 +1,9 @@
|
|||||||
using Wabbajack.DTOs;
|
using Wabbajack.DTOs;
|
||||||
using Wabbajack.Paths;
|
using Wabbajack.Paths;
|
||||||
|
|
||||||
namespace Wabbajack.App.Messages
|
namespace Wabbajack.App.Messages;
|
||||||
|
|
||||||
|
public record StartInstallation(AbsolutePath ModListPath, AbsolutePath Install, AbsolutePath Download,
|
||||||
|
ModlistMetadata? Metadata)
|
||||||
{
|
{
|
||||||
public record StartInstallation(AbsolutePath ModListPath, AbsolutePath Install, AbsolutePath Download, ModlistMetadata? Metadata)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -8,75 +8,74 @@ using Wabbajack.DTOs.SavedSettings;
|
|||||||
using Wabbajack.Paths;
|
using Wabbajack.Paths;
|
||||||
using Wabbajack.Paths.IO;
|
using Wabbajack.Paths.IO;
|
||||||
|
|
||||||
namespace Wabbajack.App.Models
|
namespace Wabbajack.App.Models;
|
||||||
|
|
||||||
|
public class InstallationStateManager
|
||||||
{
|
{
|
||||||
public class InstallationStateManager
|
private readonly DTOSerializer _dtos;
|
||||||
|
private readonly ILogger<InstallationStateManager> _logger;
|
||||||
|
|
||||||
|
public InstallationStateManager(ILogger<InstallationStateManager> logger, DTOSerializer dtos)
|
||||||
{
|
{
|
||||||
private static AbsolutePath Path => KnownFolders.WabbajackAppLocal.Combine("install-configuration-state.json");
|
_dtos = dtos;
|
||||||
private readonly DTOSerializer _dtos;
|
_logger = logger;
|
||||||
private readonly ILogger<InstallationStateManager> _logger;
|
}
|
||||||
|
|
||||||
public InstallationStateManager(ILogger<InstallationStateManager> logger, DTOSerializer dtos)
|
private static AbsolutePath Path => KnownFolders.WabbajackAppLocal.Combine("install-configuration-state.json");
|
||||||
|
|
||||||
|
public async Task<InstallationConfigurationSetting> GetLastState()
|
||||||
|
{
|
||||||
|
var state = await GetAll();
|
||||||
|
var result = state.Settings.FirstOrDefault(s => s.ModList == state.LastModlist) ??
|
||||||
|
new InstallationConfigurationSetting();
|
||||||
|
|
||||||
|
if (!result.ModList.FileExists())
|
||||||
|
return new InstallationConfigurationSetting();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetLastState(InstallationConfigurationSetting setting)
|
||||||
|
{
|
||||||
|
if (!setting.ModList.FileExists())
|
||||||
{
|
{
|
||||||
_dtos = dtos;
|
_logger.LogCritical("ModList path doesn't exist, not saving settings");
|
||||||
_logger = logger;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<InstallationConfigurationSetting> GetLastState()
|
var state = await GetAll();
|
||||||
{
|
state.LastModlist = setting.ModList;
|
||||||
var state = await GetAll();
|
state.Settings = state.Settings
|
||||||
var result = state.Settings.FirstOrDefault(s => s.ModList == state.LastModlist) ??
|
.Where(s => s.ModList != setting.ModList)
|
||||||
new InstallationConfigurationSetting();
|
.Append(setting)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
if (!result.ModList.FileExists())
|
await using var fs = Path.Open(FileMode.Create, FileAccess.Write, FileShare.None);
|
||||||
return new InstallationConfigurationSetting();
|
await _dtos.Serialize(state, fs, true);
|
||||||
return result;
|
}
|
||||||
|
|
||||||
|
public async Task<InstallConfigurationState> GetAll()
|
||||||
|
{
|
||||||
|
if (!Path.FileExists()) return new InstallConfigurationState();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var fs = Path.Open(FileMode.Open);
|
||||||
|
return (await _dtos.DeserializeAsync<InstallConfigurationState>(fs))!;
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
public async Task SetLastState(InstallationConfigurationSetting setting)
|
|
||||||
{
|
{
|
||||||
if (!setting.ModList.FileExists())
|
_logger.LogError(ex, "While loading json");
|
||||||
{
|
return new InstallConfigurationState();
|
||||||
_logger.LogCritical("ModList path doesn't exist, not saving settings");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var state = await GetAll();
|
|
||||||
state.LastModlist = setting.ModList;
|
|
||||||
state.Settings = state.Settings
|
|
||||||
.Where(s => s.ModList != setting.ModList)
|
|
||||||
.Append(setting)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
await using var fs = Path.Open(FileMode.Create, FileAccess.Write, FileShare.None);
|
|
||||||
await _dtos.Serialize(state, fs, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<InstallConfigurationState> GetAll()
|
|
||||||
{
|
|
||||||
if (!Path.FileExists()) return new InstallConfigurationState();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await using var fs = Path.Open(FileMode.Open);
|
|
||||||
return (await _dtos.DeserializeAsync<InstallConfigurationState>(fs))!;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "While loading json");
|
|
||||||
return new InstallConfigurationState();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<InstallationConfigurationSetting?> Get(AbsolutePath modListPath)
|
|
||||||
{
|
|
||||||
return (await GetAll()).Settings.FirstOrDefault(f => f.ModList == modListPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<InstallationConfigurationSetting?> GetByInstallFolder(AbsolutePath folder)
|
|
||||||
{
|
|
||||||
return (await GetAll()).Settings.FirstOrDefault(f => f.Install == folder);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<InstallationConfigurationSetting?> Get(AbsolutePath modListPath)
|
||||||
|
{
|
||||||
|
return (await GetAll()).Settings.FirstOrDefault(f => f.ModList == modListPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<InstallationConfigurationSetting?> GetByInstallFolder(AbsolutePath folder)
|
||||||
|
{
|
||||||
|
return (await GetAll()).Settings.FirstOrDefault(f => f.Install == folder);
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,10 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
using System.Reactive.Linq;
|
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using ReactiveUI.Fody.Helpers;
|
using ReactiveUI.Fody.Helpers;
|
||||||
using Wabbajack.App.ViewModels;
|
|
||||||
|
|
||||||
namespace Wabbajack.App.Models;
|
namespace Wabbajack.App.Models;
|
||||||
|
|
||||||
@ -12,34 +10,31 @@ public class LoadingLock : ReactiveObject, IDisposable
|
|||||||
{
|
{
|
||||||
private readonly CompositeDisposable _disposable;
|
private readonly CompositeDisposable _disposable;
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public int LoadLevel { get; private set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public bool IsLoading { get; private set; }
|
|
||||||
|
|
||||||
public LoadingLock()
|
public LoadingLock()
|
||||||
{
|
{
|
||||||
_disposable = new CompositeDisposable();
|
_disposable = new CompositeDisposable();
|
||||||
|
|
||||||
this.WhenAnyValue(vm => vm.LoadLevel)
|
this.WhenAnyValue(vm => vm.LoadLevel)
|
||||||
.Subscribe(v => IsLoading = v > 0)
|
.Subscribe(v => IsLoading = v > 0)
|
||||||
.DisposeWith(_disposable);
|
.DisposeWith(_disposable);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IDisposable WithLoading()
|
[Reactive] public int LoadLevel { get; private set; }
|
||||||
{
|
|
||||||
Dispatcher.UIThread.Post(() => { LoadLevel++;}, DispatcherPriority.Background);
|
[Reactive] public bool IsLoading { get; private set; }
|
||||||
return Disposable.Create(() =>
|
|
||||||
{
|
|
||||||
Dispatcher.UIThread.Post(() => { LoadLevel--;}, DispatcherPriority.Background);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
_disposable.Dispose();
|
_disposable.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IDisposable WithLoading()
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() => { LoadLevel++; }, DispatcherPriority.Background);
|
||||||
|
return Disposable.Create(() =>
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() => { LoadLevel--; }, DispatcherPriority.Background);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
@ -3,7 +3,6 @@ using System.IO;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using JetBrains.Annotations;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Wabbajack.Common;
|
using Wabbajack.Common;
|
||||||
using Wabbajack.DTOs.JsonConverters;
|
using Wabbajack.DTOs.JsonConverters;
|
||||||
@ -26,7 +25,10 @@ public class SettingsManager
|
|||||||
_configuration.SavedSettingsLocation.CreateDirectory();
|
_configuration.SavedSettingsLocation.CreateDirectory();
|
||||||
}
|
}
|
||||||
|
|
||||||
private AbsolutePath GetPath(string key) => _configuration.SavedSettingsLocation.Combine(key).WithExtension(Ext.Json);
|
private AbsolutePath GetPath(string key)
|
||||||
|
{
|
||||||
|
return _configuration.SavedSettingsLocation.Combine(key).WithExtension(Ext.Json);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task Save<T>(string key, T value)
|
public async Task Save<T>(string key, T value)
|
||||||
{
|
{
|
||||||
@ -35,22 +37,21 @@ public class SettingsManager
|
|||||||
{
|
{
|
||||||
await JsonSerializer.SerializeAsync(s, value, _dtos.Options);
|
await JsonSerializer.SerializeAsync(s, value, _dtos.Options);
|
||||||
}
|
}
|
||||||
|
|
||||||
await tmp.MoveToAsync(GetPath(key), true, CancellationToken.None);
|
await tmp.MoveToAsync(GetPath(key), true, CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<T> Load<T>(string key)
|
public async Task<T> Load<T>(string key)
|
||||||
where T : new()
|
where T : new()
|
||||||
{
|
{
|
||||||
var path = GetPath(key);
|
var path = GetPath(key);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (path.FileExists())
|
if (path.FileExists())
|
||||||
{
|
|
||||||
await using (var s = path.Open(FileMode.Open))
|
await using (var s = path.Open(FileMode.Open))
|
||||||
{
|
{
|
||||||
return (await JsonSerializer.DeserializeAsync<T>(s, _dtos.Options))!;
|
return (await JsonSerializer.DeserializeAsync<T>(s, _dtos.Options))!;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -3,28 +3,31 @@ using Avalonia;
|
|||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
using CefNet;
|
using CefNet;
|
||||||
|
|
||||||
namespace Wabbajack.App
|
namespace Wabbajack.App;
|
||||||
|
|
||||||
|
internal class Program
|
||||||
{
|
{
|
||||||
|
// Initialization code. Don't use any Avalonia, third-party APIs or any
|
||||||
class Program
|
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
||||||
|
// yet and stuff might break.
|
||||||
|
[STAThread]
|
||||||
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
// Initialization code. Don't use any Avalonia, third-party APIs or any
|
BuildAvaloniaApp()
|
||||||
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
|
||||||
// yet and stuff might break.
|
|
||||||
[STAThread]
|
|
||||||
public static void Main(string[] args) => BuildAvaloniaApp()
|
|
||||||
.StartWithCefNetApplicationLifetime(args);
|
.StartWithCefNetApplicationLifetime(args);
|
||||||
|
|
||||||
// Avalonia configuration, don't remove; also used by visual designer.
|
|
||||||
public static AppBuilder BuildAvaloniaApp()
|
|
||||||
=> AppBuilder.Configure<App>()
|
|
||||||
.UsePlatformDetect()
|
|
||||||
.AfterSetup(AfterSetupCallback)
|
|
||||||
.LogToTrace()
|
|
||||||
.UseReactiveUI();
|
|
||||||
|
|
||||||
private static void AfterSetupCallback(AppBuilder obj)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Avalonia configuration, don't remove; also used by visual designer.
|
||||||
|
public static AppBuilder BuildAvaloniaApp()
|
||||||
|
{
|
||||||
|
return AppBuilder.Configure<App>()
|
||||||
|
.UsePlatformDetect()
|
||||||
|
.AfterSetup(AfterSetupCallback)
|
||||||
|
.LogToTrace()
|
||||||
|
.UseReactiveUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AfterSetupCallback(AppBuilder obj)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -54,7 +54,7 @@
|
|||||||
x:Name="OnlyInstalledCheckbox"
|
x:Name="OnlyInstalledCheckbox"
|
||||||
Margin="10,0,10,0"
|
Margin="10,0,10,0"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Content="Only Installed"/>
|
Content="Only Installed" />
|
||||||
<Button
|
<Button
|
||||||
x:Name="ClearFiltersButton"
|
x:Name="ClearFiltersButton"
|
||||||
Margin="0,0,10,0">
|
Margin="0,0,10,0">
|
||||||
|
@ -1,44 +1,39 @@
|
|||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using Wabbajack.App.ViewModels;
|
|
||||||
using Wabbajack.App.Views;
|
using Wabbajack.App.Views;
|
||||||
|
|
||||||
namespace Wabbajack.App.Screens
|
namespace Wabbajack.App.Screens;
|
||||||
|
|
||||||
|
public partial class BrowseView : ScreenBase<BrowseViewModel>
|
||||||
{
|
{
|
||||||
public partial class BrowseView : ScreenBase<BrowseViewModel>
|
public BrowseView()
|
||||||
{
|
{
|
||||||
public BrowseView()
|
InitializeComponent();
|
||||||
|
this.WhenActivated(disposables =>
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
this.OneWayBind(ViewModel, vm => vm.ModLists, view => view.GalleryList.Items)
|
||||||
this.WhenActivated(disposables =>
|
.DisposeWith(disposables);
|
||||||
{
|
|
||||||
this.OneWayBind(ViewModel, vm => vm.ModLists, view => view.GalleryList.Items)
|
|
||||||
.DisposeWith(disposables);
|
|
||||||
|
|
||||||
this.Bind(ViewModel, vm => vm.SearchText, view => view.SearchBox.Text)
|
this.Bind(ViewModel, vm => vm.SearchText, view => view.SearchBox.Text)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.OneWayBind(ViewModel, vm => vm.GamesList, view => view.GamesList.Items)
|
this.OneWayBind(ViewModel, vm => vm.GamesList, view => view.GamesList.Items)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.Bind(ViewModel, vm => vm.SelectedGame, view => view.GamesList.SelectedItem)
|
this.Bind(ViewModel, vm => vm.SelectedGame, view => view.GamesList.SelectedItem)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.BindCommand(ViewModel, vm => vm.ResetFiltersCommand, view => view.ClearFiltersButton)
|
this.BindCommand(ViewModel, vm => vm.ResetFiltersCommand, view => view.ClearFiltersButton)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.Bind(ViewModel, vm => vm.OnlyInstalledGames, view => view.OnlyInstalledCheckbox.IsChecked)
|
this.Bind(ViewModel, vm => vm.OnlyInstalledGames, view => view.OnlyInstalledCheckbox.IsChecked)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.Bind(ViewModel, vm => vm.OnlyUtilityLists, view => view.ShowUtilityLists.IsChecked)
|
this.Bind(ViewModel, vm => vm.OnlyUtilityLists, view => view.ShowUtilityLists.IsChecked)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.Bind(ViewModel, vm => vm.ShowNSFW, view => view.ShowNSFW.IsChecked)
|
this.Bind(ViewModel, vm => vm.ShowNSFW, view => view.ShowNSFW.IsChecked)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -8,275 +8,255 @@ using System.Reactive;
|
|||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Threading;
|
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
using DynamicData.Binding;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using ReactiveUI.Fody.Helpers;
|
using ReactiveUI.Fody.Helpers;
|
||||||
using Wabbajack.App.Controls;
|
using Wabbajack.App.Controls;
|
||||||
|
using Wabbajack.App.Models;
|
||||||
using Wabbajack.App.ViewModels;
|
using Wabbajack.App.ViewModels;
|
||||||
using Wabbajack.Common;
|
using Wabbajack.Common;
|
||||||
using Wabbajack.DTOs;
|
|
||||||
using Wabbajack.Networking.WabbajackClientApi;
|
|
||||||
using DynamicData.Binding;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Wabbajack.App.Models;
|
|
||||||
using Wabbajack.Downloaders;
|
using Wabbajack.Downloaders;
|
||||||
using Wabbajack.Downloaders.GameFile;
|
using Wabbajack.Downloaders.GameFile;
|
||||||
|
using Wabbajack.DTOs;
|
||||||
using Wabbajack.DTOs.JsonConverters;
|
using Wabbajack.DTOs.JsonConverters;
|
||||||
using Wabbajack.Installer;
|
using Wabbajack.Networking.WabbajackClientApi;
|
||||||
using Wabbajack.Paths;
|
using Wabbajack.Paths;
|
||||||
using Wabbajack.Paths.IO;
|
using Wabbajack.Paths.IO;
|
||||||
using Wabbajack.RateLimiter;
|
using Wabbajack.RateLimiter;
|
||||||
using Wabbajack.VFS;
|
using Wabbajack.VFS;
|
||||||
|
|
||||||
namespace Wabbajack.App.Screens
|
namespace Wabbajack.App.Screens;
|
||||||
|
|
||||||
|
public class BrowseViewModel : ViewModelBase, IActivatableViewModel
|
||||||
{
|
{
|
||||||
public class BrowseViewModel : ViewModelBase, IActivatableViewModel
|
private readonly Configuration _configuration;
|
||||||
|
private readonly DownloadDispatcher _dispatcher;
|
||||||
|
private readonly IResource<DownloadDispatcher> _dispatcherLimiter;
|
||||||
|
private readonly DTOSerializer _dtos;
|
||||||
|
public readonly ReadOnlyObservableCollection<GameSelectorItemViewModel> _filteredGamesList;
|
||||||
|
|
||||||
|
public readonly ReadOnlyObservableCollection<BrowseItemViewModel> _filteredModLists;
|
||||||
|
private readonly GameLocator _gameLocator;
|
||||||
|
private readonly FileHashCache _hashCache;
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly IResource<HttpClient> _limiter;
|
||||||
|
private readonly ILogger<BrowseViewModel> _logger;
|
||||||
|
private readonly Client _wjClient;
|
||||||
|
|
||||||
|
private readonly SourceCache<GameSelectorItemViewModel, string> _gamesList = new(x => x.Name);
|
||||||
|
|
||||||
|
private readonly SourceCache<BrowseItemViewModel, string> _modLists = new(x => x.MachineURL);
|
||||||
|
|
||||||
|
public BrowseViewModel(ILogger<BrowseViewModel> logger, Client wjClient, HttpClient httpClient,
|
||||||
|
IResource<HttpClient> limiter, FileHashCache hashCache,
|
||||||
|
IResource<DownloadDispatcher> dispatcherLimiter, DownloadDispatcher dispatcher, GameLocator gameLocator,
|
||||||
|
DTOSerializer dtos, Configuration configuration)
|
||||||
{
|
{
|
||||||
private readonly Client _wjClient;
|
LoadingLock = new LoadingLock();
|
||||||
private readonly ILogger<BrowseViewModel> _logger;
|
Activator = new ViewModelActivator();
|
||||||
private readonly HttpClient _httpClient;
|
_wjClient = wjClient;
|
||||||
private readonly IResource<HttpClient> _limiter;
|
_logger = logger;
|
||||||
private readonly FileHashCache _hashCache;
|
_httpClient = httpClient;
|
||||||
private readonly Configuration _configuration;
|
_limiter = limiter;
|
||||||
private readonly DownloadDispatcher _dispatcher;
|
_hashCache = hashCache;
|
||||||
private readonly IResource<DownloadDispatcher> _dispatcherLimiter;
|
_configuration = configuration;
|
||||||
|
_dispatcher = dispatcher;
|
||||||
|
_dispatcherLimiter = dispatcherLimiter;
|
||||||
|
_gameLocator = gameLocator;
|
||||||
|
_dtos = dtos;
|
||||||
|
|
||||||
private SourceCache<BrowseItemViewModel, string> _modLists = new(x => x.MachineURL);
|
|
||||||
|
|
||||||
public readonly ReadOnlyObservableCollection<BrowseItemViewModel> _filteredModLists;
|
|
||||||
public ReadOnlyObservableCollection<BrowseItemViewModel> ModLists => _filteredModLists;
|
|
||||||
|
|
||||||
private SourceCache<GameSelectorItemViewModel, string> _gamesList = new(x => x.Name);
|
|
||||||
public readonly ReadOnlyObservableCollection<GameSelectorItemViewModel> _filteredGamesList;
|
|
||||||
private readonly GameLocator _gameLocator;
|
|
||||||
private readonly DTOSerializer _dtos;
|
|
||||||
public ReadOnlyObservableCollection<GameSelectorItemViewModel> GamesList => _filteredGamesList;
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public GameSelectorItemViewModel? SelectedGame { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public string SearchText { get; set; }
|
|
||||||
|
|
||||||
[Reactive] public bool OnlyInstalledGames { get; set; } = false;
|
var searchTextPredicates = this.ObservableForProperty(vm => vm.SearchText)
|
||||||
|
.Select(change => change.Value)
|
||||||
|
.StartWith("")
|
||||||
|
.Select<string, Func<BrowseItemViewModel, bool>>(txt =>
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(txt)) return _ => true;
|
||||||
|
return item => item.Title.Contains(txt, StringComparison.InvariantCultureIgnoreCase) ||
|
||||||
|
item.Description.Contains(txt, StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
});
|
||||||
|
|
||||||
[Reactive] public bool OnlyUtilityLists { get; set; } = false;
|
|
||||||
|
|
||||||
[Reactive] public bool ShowNSFW { get; set; } = false;
|
_gamesList.Edit(e =>
|
||||||
|
|
||||||
[Reactive] public bool IsLoading { get; set; } = false;
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public LoadingLock LoadingLock { get; set; }
|
|
||||||
|
|
||||||
public BrowseViewModel(ILogger<BrowseViewModel> logger, Client wjClient, HttpClient httpClient, IResource<HttpClient> limiter, FileHashCache hashCache,
|
|
||||||
IResource<DownloadDispatcher> dispatcherLimiter, DownloadDispatcher dispatcher, GameLocator gameLocator, DTOSerializer dtos, Configuration configuration)
|
|
||||||
{
|
{
|
||||||
LoadingLock = new LoadingLock();
|
e.Clear();
|
||||||
Activator = new ViewModelActivator();
|
foreach (var game in GameRegistry.Games.Keys) e.AddOrUpdate(new GameSelectorItemViewModel(game));
|
||||||
_wjClient = wjClient;
|
});
|
||||||
_logger = logger;
|
|
||||||
_httpClient = httpClient;
|
|
||||||
_limiter = limiter;
|
|
||||||
_hashCache = hashCache;
|
|
||||||
_configuration = configuration;
|
|
||||||
_dispatcher = dispatcher;
|
|
||||||
_dispatcherLimiter = dispatcherLimiter;
|
|
||||||
_gameLocator = gameLocator;
|
|
||||||
_dtos = dtos;
|
|
||||||
|
|
||||||
|
|
||||||
IObservable<Func<BrowseItemViewModel, bool>> searchTextPredicates = this.ObservableForProperty(vm => vm.SearchText)
|
|
||||||
.Select(change => change.Value)
|
|
||||||
.StartWith("")
|
|
||||||
.Select<string, Func<BrowseItemViewModel, bool>>(txt =>
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(txt)) return _ => true;
|
|
||||||
return item => item.Title.Contains(txt, StringComparison.InvariantCultureIgnoreCase) ||
|
|
||||||
item.Description.Contains(txt, StringComparison.InvariantCultureIgnoreCase);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
_gamesList.Connect()
|
||||||
_gamesList.Edit(e =>
|
.ObserveOn(RxApp.MainThreadScheduler)
|
||||||
|
.Sort(Comparer<GameSelectorItemViewModel>.Create((a, b) => string.CompareOrdinal(a.Name, b.Name)))
|
||||||
|
.Bind(out _filteredGamesList)
|
||||||
|
.Subscribe();
|
||||||
|
|
||||||
|
var gameFilter = this.ObservableForProperty(vm => vm.SelectedGame)
|
||||||
|
.Select(v => v.Value)
|
||||||
|
.Select<GameSelectorItemViewModel?, Func<BrowseItemViewModel, bool>>(selected =>
|
||||||
{
|
{
|
||||||
e.Clear();
|
if (selected == null) return _ => true;
|
||||||
foreach (var game in GameRegistry.Games.Keys)
|
return item => item.Game == selected.Game;
|
||||||
{
|
})
|
||||||
e.AddOrUpdate(new GameSelectorItemViewModel(game));
|
.StartWith(_ => true);
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
_gamesList.Connect()
|
var onlyInstalledGamesFilter = this.ObservableForProperty(vm => vm.OnlyInstalledGames)
|
||||||
.ObserveOn(RxApp.MainThreadScheduler)
|
.Select(v => v.Value)
|
||||||
.Sort(Comparer<GameSelectorItemViewModel>.Create((a, b) => string.CompareOrdinal(a.Name, b.Name)))
|
.Select<bool, Func<BrowseItemViewModel, bool>>(onlyInstalled =>
|
||||||
.Bind(out _filteredGamesList)
|
|
||||||
.Subscribe();
|
|
||||||
|
|
||||||
IObservable<Func<BrowseItemViewModel, bool>> gameFilter = this.ObservableForProperty(vm => vm.SelectedGame)
|
|
||||||
.Select(v => v.Value)
|
|
||||||
.Select<GameSelectorItemViewModel?, Func<BrowseItemViewModel, bool>>(selected =>
|
|
||||||
{
|
|
||||||
if (selected == null) return _ => true;
|
|
||||||
return item => item.Game == selected.Game;
|
|
||||||
})
|
|
||||||
.StartWith(_ => true);
|
|
||||||
|
|
||||||
IObservable<Func<BrowseItemViewModel, bool>> onlyInstalledGamesFilter = this.ObservableForProperty(vm => vm.OnlyInstalledGames)
|
|
||||||
.Select(v => v.Value)
|
|
||||||
.Select<bool, Func<BrowseItemViewModel, bool>>(onlyInstalled =>
|
|
||||||
{
|
|
||||||
if (onlyInstalled == false) return _ => true;
|
|
||||||
return item => _gameLocator.IsInstalled(item.Game);
|
|
||||||
})
|
|
||||||
.StartWith(_ => true);
|
|
||||||
|
|
||||||
IObservable<Func<BrowseItemViewModel, bool>> onlyUtilityListsFilter = this.ObservableForProperty(vm => vm.OnlyUtilityLists)
|
|
||||||
.Select(v => v.Value)
|
|
||||||
.Select<bool, Func<BrowseItemViewModel, bool>>(utility =>
|
|
||||||
{
|
|
||||||
if (utility == false) return item => item.IsUtilityList == false ;
|
|
||||||
return item => item.IsUtilityList;
|
|
||||||
})
|
|
||||||
.StartWith(item => item.IsUtilityList == false);
|
|
||||||
|
|
||||||
IObservable<Func<BrowseItemViewModel, bool>> showNSFWFilter = this.ObservableForProperty(vm => vm.ShowNSFW)
|
|
||||||
.Select(v => v.Value)
|
|
||||||
.Select<bool, Func<BrowseItemViewModel, bool>>(showNSFW =>
|
|
||||||
{
|
|
||||||
return item => item.IsNSFW == showNSFW;
|
|
||||||
})
|
|
||||||
.StartWith(item => item.IsNSFW == false);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
_modLists.Connect()
|
|
||||||
.ObserveOn(RxApp.MainThreadScheduler)
|
|
||||||
.Filter(searchTextPredicates)
|
|
||||||
.Filter(gameFilter)
|
|
||||||
.Filter(onlyInstalledGamesFilter)
|
|
||||||
.Filter(onlyUtilityListsFilter)
|
|
||||||
.Filter(showNSFWFilter)
|
|
||||||
.Bind(out _filteredModLists)
|
|
||||||
.Subscribe();
|
|
||||||
|
|
||||||
ResetFiltersCommand = ReactiveCommand.Create(() =>
|
|
||||||
{
|
{
|
||||||
SelectedGame = null;
|
if (onlyInstalled == false) return _ => true;
|
||||||
SearchText = "";
|
return item => _gameLocator.IsInstalled(item.Game);
|
||||||
});
|
})
|
||||||
|
.StartWith(_ => true);
|
||||||
|
|
||||||
|
var onlyUtilityListsFilter = this.ObservableForProperty(vm => vm.OnlyUtilityLists)
|
||||||
this.WhenActivated(disposables =>
|
.Select(v => v.Value)
|
||||||
|
.Select<bool, Func<BrowseItemViewModel, bool>>(utility =>
|
||||||
{
|
{
|
||||||
LoadSettings().FireAndForget();
|
if (utility == false) return item => item.IsUtilityList == false;
|
||||||
LoadData().FireAndForget();
|
return item => item.IsUtilityList;
|
||||||
|
})
|
||||||
|
.StartWith(item => item.IsUtilityList == false);
|
||||||
|
|
||||||
Disposable.Create(() =>
|
var showNSFWFilter = this.ObservableForProperty(vm => vm.ShowNSFW)
|
||||||
{
|
.Select(v => v.Value)
|
||||||
SaveSettings().FireAndForget();
|
.Select<bool, Func<BrowseItemViewModel, bool>>(showNSFW => { return item => item.IsNSFW == showNSFW; })
|
||||||
}).DisposeWith(disposables);
|
.StartWith(item => item.IsNSFW == false);
|
||||||
|
|
||||||
/*
|
|
||||||
var searchTextFilter = this.ObservableForProperty(view => view.SearchText)
|
|
||||||
.Select<IObservedChange<BrowseViewModel, string>, Func<>>(text =>
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(text.Value))
|
|
||||||
return lst => true;
|
|
||||||
return
|
|
||||||
})*/
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[Reactive]
|
_modLists.Connect()
|
||||||
public ReactiveCommand<Unit, Unit> ResetFiltersCommand { get; set; }
|
.ObserveOn(RxApp.MainThreadScheduler)
|
||||||
|
.Filter(searchTextPredicates)
|
||||||
|
.Filter(gameFilter)
|
||||||
|
.Filter(onlyInstalledGamesFilter)
|
||||||
|
.Filter(onlyUtilityListsFilter)
|
||||||
|
.Filter(showNSFWFilter)
|
||||||
|
.Bind(out _filteredModLists)
|
||||||
|
.Subscribe();
|
||||||
|
|
||||||
private async Task LoadData()
|
ResetFiltersCommand = ReactiveCommand.Create(() =>
|
||||||
{
|
{
|
||||||
using var _ = LoadingLock.WithLoading();
|
SelectedGame = null;
|
||||||
var modlists = await _wjClient.LoadLists();
|
SearchText = "";
|
||||||
var summaries = (await _wjClient.GetListStatuses()).ToDictionary(m => m.MachineURL);
|
});
|
||||||
var vms = modlists.Select(m =>
|
|
||||||
{
|
|
||||||
if (!summaries.TryGetValue(m.Links.MachineURL, out var summary))
|
|
||||||
{
|
|
||||||
summary = new ModListSummary();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new BrowseItemViewModel(m, summary, _httpClient, _limiter, _hashCache, _configuration, _dispatcher, _dispatcherLimiter, _gameLocator, _dtos, _logger);
|
|
||||||
});
|
|
||||||
|
|
||||||
_modLists.Edit(lsts =>
|
|
||||||
{
|
|
||||||
lsts.Clear();
|
|
||||||
lsts.AddOrUpdate(vms);
|
|
||||||
});
|
|
||||||
|
|
||||||
_logger.LogInformation("Loaded data for {Count} modlists", _modLists.Count);
|
this.WhenActivated(disposables =>
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadSettings()
|
|
||||||
{
|
{
|
||||||
using var _ = LoadingLock.WithLoading();
|
LoadSettings().FireAndForget();
|
||||||
try
|
LoadData().FireAndForget();
|
||||||
{
|
|
||||||
if (SavedSettingsLocation.FileExists())
|
|
||||||
{
|
|
||||||
await using var stream = SavedSettingsLocation.Open(FileMode.Open);
|
|
||||||
var data = (await JsonSerializer.DeserializeAsync<SavedSettings>(stream))!;
|
|
||||||
SearchText = data.SearchText;
|
|
||||||
|
|
||||||
SelectedGame = data.SelectedGame == null ? null : _gamesList.Lookup(data.SelectedGame.Value.MetaData().HumanFriendlyGameName).Value;
|
|
||||||
|
|
||||||
ShowNSFW = data.ShowNSFW;
|
Disposable.Create(() => { SaveSettings().FireAndForget(); }).DisposeWith(disposables);
|
||||||
OnlyUtilityLists = data.OnlyUtility;
|
|
||||||
OnlyInstalledGames = data.OnlyInstalled;
|
/*
|
||||||
}
|
var searchTextFilter = this.ObservableForProperty(view => view.SearchText)
|
||||||
}
|
.Select<IObservedChange<BrowseViewModel, string>, Func<>>(text =>
|
||||||
catch (Exception ex)
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text.Value))
|
||||||
|
return lst => true;
|
||||||
|
return
|
||||||
|
})*/
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReadOnlyObservableCollection<BrowseItemViewModel> ModLists => _filteredModLists;
|
||||||
|
public ReadOnlyObservableCollection<GameSelectorItemViewModel> GamesList => _filteredGamesList;
|
||||||
|
|
||||||
|
[Reactive] public GameSelectorItemViewModel? SelectedGame { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public string SearchText { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public bool OnlyInstalledGames { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public bool OnlyUtilityLists { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public bool ShowNSFW { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public bool IsLoading { get; set; } = false;
|
||||||
|
|
||||||
|
[Reactive] public LoadingLock LoadingLock { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
[Reactive] public ReactiveCommand<Unit, Unit> ResetFiltersCommand { get; set; }
|
||||||
|
|
||||||
|
private AbsolutePath SavedSettingsLocation => _configuration.SavedSettingsLocation.Combine("browse_view.json");
|
||||||
|
|
||||||
|
private async Task LoadData()
|
||||||
|
{
|
||||||
|
using var _ = LoadingLock.WithLoading();
|
||||||
|
var modlists = await _wjClient.LoadLists();
|
||||||
|
var summaries = (await _wjClient.GetListStatuses()).ToDictionary(m => m.MachineURL);
|
||||||
|
var vms = modlists.Select(m =>
|
||||||
|
{
|
||||||
|
if (!summaries.TryGetValue(m.Links.MachineURL, out var summary)) summary = new ModListSummary();
|
||||||
|
|
||||||
|
return new BrowseItemViewModel(m, summary, _httpClient, _limiter, _hashCache, _configuration, _dispatcher,
|
||||||
|
_dispatcherLimiter, _gameLocator, _dtos, _logger);
|
||||||
|
});
|
||||||
|
|
||||||
|
_modLists.Edit(lsts =>
|
||||||
|
{
|
||||||
|
lsts.Clear();
|
||||||
|
lsts.AddOrUpdate(vms);
|
||||||
|
});
|
||||||
|
|
||||||
|
_logger.LogInformation("Loaded data for {Count} modlists", _modLists.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadSettings()
|
||||||
|
{
|
||||||
|
using var _ = LoadingLock.WithLoading();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (SavedSettingsLocation.FileExists())
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "While loading gallery browse settings");
|
await using var stream = SavedSettingsLocation.Open(FileMode.Open);
|
||||||
|
var data = (await JsonSerializer.DeserializeAsync<SavedSettings>(stream))!;
|
||||||
|
SearchText = data.SearchText;
|
||||||
|
|
||||||
|
SelectedGame = data.SelectedGame == null
|
||||||
|
? null
|
||||||
|
: _gamesList.Lookup(data.SelectedGame.Value.MetaData().HumanFriendlyGameName).Value;
|
||||||
|
|
||||||
|
ShowNSFW = data.ShowNSFW;
|
||||||
|
OnlyUtilityLists = data.OnlyUtility;
|
||||||
|
OnlyInstalledGames = data.OnlyInstalled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
private async Task SaveSettings()
|
|
||||||
{
|
{
|
||||||
try
|
_logger.LogWarning(ex, "While loading gallery browse settings");
|
||||||
{
|
|
||||||
var settings = new SavedSettings
|
|
||||||
{
|
|
||||||
SearchText = SearchText,
|
|
||||||
OnlyInstalled = OnlyInstalledGames,
|
|
||||||
OnlyUtility = OnlyUtilityLists,
|
|
||||||
ShowNSFW = ShowNSFW,
|
|
||||||
SelectedGame = SelectedGame?.Game
|
|
||||||
};
|
|
||||||
SavedSettingsLocation.Parent.CreateDirectory();
|
|
||||||
await using var stream = SavedSettingsLocation.Open(FileMode.Create, FileAccess.Write, FileShare.None);
|
|
||||||
await JsonSerializer.SerializeAsync(stream, settings);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "While saving gallery browse settings");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private AbsolutePath SavedSettingsLocation => _configuration.SavedSettingsLocation.Combine("browse_view.json");
|
|
||||||
|
|
||||||
private class SavedSettings
|
|
||||||
{
|
|
||||||
public string SearchText { get; set; }
|
|
||||||
public bool ShowNSFW { get; set; }
|
|
||||||
public bool OnlyUtility { get; set; }
|
|
||||||
public bool OnlyInstalled { get; set; }
|
|
||||||
public Game? SelectedGame { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task SaveSettings()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var settings = new SavedSettings
|
||||||
|
{
|
||||||
|
SearchText = SearchText,
|
||||||
|
OnlyInstalled = OnlyInstalledGames,
|
||||||
|
OnlyUtility = OnlyUtilityLists,
|
||||||
|
ShowNSFW = ShowNSFW,
|
||||||
|
SelectedGame = SelectedGame?.Game
|
||||||
|
};
|
||||||
|
SavedSettingsLocation.Parent.CreateDirectory();
|
||||||
|
await using var stream = SavedSettingsLocation.Open(FileMode.Create, FileAccess.Write, FileShare.None);
|
||||||
|
await JsonSerializer.SerializeAsync(stream, settings);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "While saving gallery browse settings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SavedSettings
|
||||||
|
{
|
||||||
|
public string SearchText { get; set; }
|
||||||
|
public bool ShowNSFW { get; set; }
|
||||||
|
public bool OnlyUtility { get; set; }
|
||||||
|
public bool OnlyInstalled { get; set; }
|
||||||
|
public Game? SelectedGame { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
@ -3,13 +3,12 @@
|
|||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
xmlns:i="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
|
||||||
xmlns:controls="clr-namespace:Wabbajack.App.Controls"
|
xmlns:controls="clr-namespace:Wabbajack.App.Controls"
|
||||||
x:Class="Wabbajack.App.Screens.CompilationView">
|
x:Class="Wabbajack.App.Screens.CompilationView">
|
||||||
<Grid RowDefinitions="40, 5, 5, *, 40">
|
<Grid RowDefinitions="40, 5, 5, *, 40">
|
||||||
<TextBlock Grid.Row="0" x:Name="StatusText" FontSize="20" FontWeight="Bold">[20/30] Installing Files</TextBlock>
|
<TextBlock Grid.Row="0" x:Name="StatusText" FontSize="20" FontWeight="Bold">[20/30] Installing Files</TextBlock>
|
||||||
<ProgressBar Grid.Row="1" x:Name="StepsProgress" Maximum="1000" Value="40"></ProgressBar>
|
<ProgressBar Grid.Row="1" x:Name="StepsProgress" Maximum="1000" Value="40" />
|
||||||
<ProgressBar Grid.Row="2" x:Name="StepProgress" Maximum="10000" Value="30"></ProgressBar>
|
<ProgressBar Grid.Row="2" x:Name="StepProgress" Maximum="10000" Value="30" />
|
||||||
<controls:LogView Grid.Row="3" x:Name="LogView"></controls:LogView>
|
<controls:LogView Grid.Row="3" x:Name="LogView" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
@ -1,6 +1,5 @@
|
|||||||
using Avalonia.Controls.Mixins;
|
using Avalonia.Controls.Mixins;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using Wabbajack.App.ViewModels;
|
|
||||||
using Wabbajack.App.Views;
|
using Wabbajack.App.Views;
|
||||||
|
|
||||||
namespace Wabbajack.App.Screens;
|
namespace Wabbajack.App.Screens;
|
||||||
@ -23,4 +22,4 @@ public partial class CompilationView : ScreenBase<CompilationViewModel>
|
|||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -16,13 +16,9 @@ namespace Wabbajack.App.Screens;
|
|||||||
|
|
||||||
public class CompilationViewModel : ViewModelBase, IReceiverMarker, IReceiver<StartCompilation>
|
public class CompilationViewModel : ViewModelBase, IReceiverMarker, IReceiver<StartCompilation>
|
||||||
{
|
{
|
||||||
|
private readonly ILogger<CompilationViewModel> _logger;
|
||||||
private readonly IServiceProvider _provider;
|
private readonly IServiceProvider _provider;
|
||||||
private ACompiler _compiler;
|
private ACompiler _compiler;
|
||||||
private readonly ILogger<CompilationViewModel> _logger;
|
|
||||||
|
|
||||||
[Reactive] public string StatusText { get; set; } = "";
|
|
||||||
[Reactive] public Percent StepsProgress { get; set; } = Percent.Zero;
|
|
||||||
[Reactive] public Percent StepProgress { get; set; } = Percent.Zero;
|
|
||||||
|
|
||||||
|
|
||||||
public CompilationViewModel(ILogger<CompilationViewModel> logger, IServiceProvider provider)
|
public CompilationViewModel(ILogger<CompilationViewModel> logger, IServiceProvider provider)
|
||||||
@ -30,9 +26,12 @@ public class CompilationViewModel : ViewModelBase, IReceiverMarker, IReceiver<St
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
_provider = provider;
|
_provider = provider;
|
||||||
Activator = new ViewModelActivator();
|
Activator = new ViewModelActivator();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Reactive] public string StatusText { get; set; } = "";
|
||||||
|
[Reactive] public Percent StepsProgress { get; set; } = Percent.Zero;
|
||||||
|
[Reactive] public Percent StepProgress { get; set; } = Percent.Zero;
|
||||||
|
|
||||||
public void Receive(StartCompilation val)
|
public void Receive(StartCompilation val)
|
||||||
{
|
{
|
||||||
if (val.Settings is MO2CompilerSettings mo2)
|
if (val.Settings is MO2CompilerSettings mo2)
|
||||||
@ -50,6 +49,7 @@ public class CompilationViewModel : ViewModelBase, IReceiverMarker, IReceiver<St
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Compile().FireAndForget();
|
Compile().FireAndForget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,48 +8,48 @@
|
|||||||
x:Class="Wabbajack.App.Screens.CompilerConfigurationView">
|
x:Class="Wabbajack.App.Screens.CompilerConfigurationView">
|
||||||
<Grid RowDefinitions="40, *, 40">
|
<Grid RowDefinitions="40, *, 40">
|
||||||
<TextBlock Grid.Row="0" x:Name="StatusText" FontSize="20" FontWeight="Bold">Compiler Configuration</TextBlock>
|
<TextBlock Grid.Row="0" x:Name="StatusText" FontSize="20" FontWeight="Bold">Compiler Configuration</TextBlock>
|
||||||
<Grid Grid.Row="1" ColumnDefinitions="Auto, *" RowDefinitions="Auto, Auto, Auto, Auto, Auto, Auto, Auto" Margin="4">
|
<Grid Grid.Row="1" ColumnDefinitions="Auto, *" RowDefinitions="Auto, Auto, Auto, Auto, Auto, Auto, Auto"
|
||||||
|
Margin="4">
|
||||||
<Label Grid.Column="0" Grid.Row="0" HorizontalAlignment="Right">Title:</Label>
|
<Label Grid.Column="0" Grid.Row="0" HorizontalAlignment="Right">Title:</Label>
|
||||||
<TextBox Grid.Column="1" Grid.Row="0" x:Name="Title"></TextBox>
|
<TextBox Grid.Column="1" Grid.Row="0" x:Name="Title" />
|
||||||
<Label Grid.Column="0" Grid.Row="1" HorizontalAlignment="Right">Settings File:</Label>
|
<Label Grid.Column="0" Grid.Row="1" HorizontalAlignment="Right">Settings File:</Label>
|
||||||
<controls:FileSelectionBox Grid.Column="1" Grid.Row="1" x:Name="SettingsFile"
|
<controls:FileSelectionBox Grid.Column="1" Grid.Row="1" x:Name="SettingsFile"
|
||||||
AllowedExtensions=".txt|.json">
|
AllowedExtensions=".txt|.json" />
|
||||||
</controls:FileSelectionBox>
|
|
||||||
<Label Grid.Column="0" Grid.Row="2" HorizontalAlignment="Right">Source:</Label>
|
<Label Grid.Column="0" Grid.Row="2" HorizontalAlignment="Right">Source:</Label>
|
||||||
<controls:FileSelectionBox Grid.Column="1" Grid.Row="2" x:Name="Source" SelectFolder="True"></controls:FileSelectionBox>
|
<controls:FileSelectionBox Grid.Column="1" Grid.Row="2" x:Name="Source" SelectFolder="True" />
|
||||||
<Label Grid.Column="0" Grid.Row="3" HorizontalAlignment="Right">Downloads Folder:</Label>
|
<Label Grid.Column="0" Grid.Row="3" HorizontalAlignment="Right">Downloads Folder:</Label>
|
||||||
<controls:FileSelectionBox Grid.Column="1" Grid.Row="3" x:Name="DownloadsFolder" SelectFolder="True"></controls:FileSelectionBox>
|
<controls:FileSelectionBox Grid.Column="1" Grid.Row="3" x:Name="DownloadsFolder" SelectFolder="True" />
|
||||||
<Label Grid.Column="0" Grid.Row="4" HorizontalAlignment="Right">Base Game:</Label>
|
<Label Grid.Column="0" Grid.Row="4" HorizontalAlignment="Right">Base Game:</Label>
|
||||||
<ComboBox Grid.Column="1" Grid.Row="4" x:Name="BaseGame">
|
<ComboBox Grid.Column="1" Grid.Row="4" x:Name="BaseGame">
|
||||||
<ComboBox.ItemTemplate>
|
<ComboBox.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<TextBlock Text="{Binding Path=HumanFriendlyGameName}"></TextBlock>
|
<TextBlock Text="{Binding Path=HumanFriendlyGameName}" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ComboBox.ItemTemplate>
|
</ComboBox.ItemTemplate>
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
<Label Grid.Column="0" Grid.Row="5" HorizontalAlignment="Right">Output Folder:</Label>
|
<Label Grid.Column="0" Grid.Row="5" HorizontalAlignment="Right">Output Folder:</Label>
|
||||||
<controls:FileSelectionBox Grid.Column="1" Grid.Row="5" x:Name="OutputFolder" SelectFolder="True"></controls:FileSelectionBox>
|
<controls:FileSelectionBox Grid.Column="1" Grid.Row="5" x:Name="OutputFolder" SelectFolder="True" />
|
||||||
|
|
||||||
<Label Grid.Column="0" Grid.Row="6" HorizontalAlignment="Right" VerticalAlignment="Top">Always Enabled:</Label>
|
<Label Grid.Column="0" Grid.Row="6" HorizontalAlignment="Right" VerticalAlignment="Top">Always Enabled:</Label>
|
||||||
<StackPanel Grid.Column="1" Grid.Row="6" Orientation="Vertical">
|
<StackPanel Grid.Column="1" Grid.Row="6" Orientation="Vertical">
|
||||||
<Button x:Name="AddAlwaysEnabled">
|
<Button x:Name="AddAlwaysEnabled">
|
||||||
<i:MaterialIcon Kind="AddCircle"></i:MaterialIcon>
|
<i:MaterialIcon Kind="AddCircle" />
|
||||||
</Button>
|
</Button>
|
||||||
<ItemsControl x:Name="AlwaysEnabledList">
|
<ItemsControl x:Name="AlwaysEnabledList">
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsControl.ItemsPanel>
|
||||||
<ItemsPanelTemplate>
|
<ItemsPanelTemplate>
|
||||||
<StackPanel Orientation="Vertical"></StackPanel>
|
<StackPanel Orientation="Vertical" />
|
||||||
</ItemsPanelTemplate>
|
</ItemsPanelTemplate>
|
||||||
</ItemsControl.ItemsPanel>
|
</ItemsControl.ItemsPanel>
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<controls:RemovableListItem></controls:RemovableListItem>
|
<controls:RemovableListItem />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid ColumnDefinitions="*, Auto, Auto" Grid.Row="2">
|
<Grid ColumnDefinitions="*, Auto, Auto" Grid.Row="2">
|
||||||
<Button Grid.Column="1" x:Name="InferSettings" Click="InferSettings_OnClick">
|
<Button Grid.Column="1" x:Name="InferSettings" Click="InferSettings_OnClick">
|
||||||
|
@ -2,7 +2,6 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
@ -12,79 +11,79 @@ using Wabbajack.App.Views;
|
|||||||
using Wabbajack.Common;
|
using Wabbajack.Common;
|
||||||
using Wabbajack.Paths;
|
using Wabbajack.Paths;
|
||||||
|
|
||||||
namespace Wabbajack.App.Screens
|
namespace Wabbajack.App.Screens;
|
||||||
|
|
||||||
|
public partial class CompilerConfigurationView : ScreenBase<CompilerConfigurationViewModel>
|
||||||
{
|
{
|
||||||
public partial class CompilerConfigurationView : ScreenBase<CompilerConfigurationViewModel>
|
public CompilerConfigurationView()
|
||||||
{
|
{
|
||||||
public CompilerConfigurationView()
|
InitializeComponent();
|
||||||
|
AddAlwaysEnabled.Command = ReactiveCommand.Create(() => AddAlwaysEnabled_Command().FireAndForget());
|
||||||
|
|
||||||
|
this.WhenActivated(disposables =>
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
this.Bind(ViewModel, vm => vm.SettingsFile, view => view.SettingsFile.SelectedPath)
|
||||||
AddAlwaysEnabled.Command = ReactiveCommand.Create(() => AddAlwaysEnabled_Command().FireAndForget());
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.WhenActivated(disposables =>
|
this.Bind(ViewModel, vm => vm.Title, view => view.Title.Text)
|
||||||
{
|
.DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.SettingsFile, view => view.SettingsFile.SelectedPath)
|
|
||||||
.DisposeWith(disposables);
|
|
||||||
|
|
||||||
this.Bind(ViewModel, vm => vm.Title, view => view.Title.Text)
|
|
||||||
.DisposeWith(disposables);
|
|
||||||
|
|
||||||
this.Bind(ViewModel, vm => vm.SettingsFile, view => view.SettingsFile.SelectedPath)
|
|
||||||
.DisposeWith(disposables);
|
|
||||||
|
|
||||||
this.Bind(ViewModel, vm => vm.Source, view => view.Source.SelectedPath)
|
|
||||||
.DisposeWith(disposables);
|
|
||||||
|
|
||||||
this.Bind(ViewModel, vm => vm.Downloads, view => view.DownloadsFolder.SelectedPath)
|
this.Bind(ViewModel, vm => vm.SettingsFile, view => view.SettingsFile.SelectedPath)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.Bind(ViewModel, vm => vm.OutputFolder, view => view.OutputFolder.SelectedPath)
|
this.Bind(ViewModel, vm => vm.Source, view => view.Source.SelectedPath)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.OneWayBind(ViewModel, vm => vm.AllGames, view => view.BaseGame.Items)
|
this.Bind(ViewModel, vm => vm.Downloads, view => view.DownloadsFolder.SelectedPath)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.Bind(ViewModel, vm => vm.BaseGame, view => view.BaseGame.SelectedItem)
|
this.Bind(ViewModel, vm => vm.OutputFolder, view => view.OutputFolder.SelectedPath)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.BindCommand(ViewModel, vm => vm.StartCompilation, view => view.StartCompilation)
|
this.OneWayBind(ViewModel, vm => vm.AllGames, view => view.BaseGame.Items)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.OneWayBind(ViewModel, vm => vm.AlwaysEnabled, view => view.AlwaysEnabledList.Items,
|
this.Bind(ViewModel, vm => vm.BaseGame, view => view.BaseGame.SelectedItem)
|
||||||
d => d!.Select(itm => new RemovableItemViewModel()
|
.DisposeWith(disposables);
|
||||||
{
|
|
||||||
Text = itm.ToString(),
|
|
||||||
DeleteCommand = ReactiveCommand.Create(() => { ViewModel?.RemoveAlwaysExcluded(itm); })
|
|
||||||
}))
|
|
||||||
.DisposeWith(disposables);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddAlwaysEnabled_Command()
|
this.BindCommand(ViewModel, vm => vm.StartCompilation, view => view.StartCompilation)
|
||||||
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
|
this.OneWayBind(ViewModel, vm => vm.AlwaysEnabled, view => view.AlwaysEnabledList.Items,
|
||||||
|
d => d!.Select(itm => new RemovableItemViewModel
|
||||||
|
{
|
||||||
|
Text = itm.ToString(),
|
||||||
|
DeleteCommand = ReactiveCommand.Create(() => { ViewModel?.RemoveAlwaysExcluded(itm); })
|
||||||
|
}))
|
||||||
|
.DisposeWith(disposables);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AddAlwaysEnabled_Command()
|
||||||
|
{
|
||||||
|
var dialog = new OpenFolderDialog
|
||||||
{
|
{
|
||||||
var dialog = new OpenFolderDialog()
|
Title = "Select a folder"
|
||||||
|
};
|
||||||
|
var result = await dialog.ShowAsync(App.MainWindow);
|
||||||
|
if (!string.IsNullOrWhiteSpace(result))
|
||||||
|
ViewModel!.AddAlwaysExcluded(result.ToAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InferSettings_OnClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||||
|
{
|
||||||
|
var dialog = new OpenFileDialog
|
||||||
{
|
{
|
||||||
Title = "Select a folder",
|
Title = "Select a modlist.txt file",
|
||||||
|
Filters = new List<FileDialogFilter>
|
||||||
|
{new() {Extensions = new List<string> {"txt"}, Name = "modlist.txt"}},
|
||||||
|
AllowMultiple = false
|
||||||
};
|
};
|
||||||
var result = await dialog.ShowAsync(App.MainWindow);
|
var result = await dialog.ShowAsync(App.MainWindow);
|
||||||
if (!string.IsNullOrWhiteSpace(result))
|
if (result is {Length: > 0})
|
||||||
ViewModel!.AddAlwaysExcluded(result.ToAbsolutePath());
|
await ViewModel!.InferSettingsFromModlistTxt(result.First().ToAbsolutePath());
|
||||||
}
|
});
|
||||||
|
|
||||||
private void InferSettings_OnClick(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
|
||||||
{
|
|
||||||
var dialog = new OpenFileDialog()
|
|
||||||
{
|
|
||||||
Title = "Select a modlist.txt file",
|
|
||||||
Filters = new List<FileDialogFilter> { new() {Extensions = new List<string> {"txt"}, Name = "modlist.txt"}},
|
|
||||||
AllowMultiple = false
|
|
||||||
};
|
|
||||||
var result = await dialog.ShowAsync(App.MainWindow);
|
|
||||||
if (result is { Length: > 0 })
|
|
||||||
await ViewModel!.InferSettingsFromModlistTxt(result.First().ToAbsolutePath());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -27,44 +27,6 @@ public class CompilerConfigurationViewModel : ViewModelBase, IReceiverMarker
|
|||||||
private readonly DTOSerializer _dtos;
|
private readonly DTOSerializer _dtos;
|
||||||
private readonly SettingsManager _settingsManager;
|
private readonly SettingsManager _settingsManager;
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public string Title { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public AbsolutePath SettingsFile { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public AbsolutePath Downloads { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public GameMetaData BaseGame { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public AbsolutePath Source { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public AbsolutePath GamePath { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public string SelectedProfile { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public AbsolutePath OutputFolder { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public IEnumerable<GameMetaData> AllGames { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public ReactiveCommand<Unit, Unit> StartCompilation { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public IEnumerable<RelativePath> AlwaysEnabled { get; set; } = Array.Empty<RelativePath>();
|
|
||||||
|
|
||||||
public AbsolutePath SettingsOutputLocation => Source.Combine(Title).WithExtension(IsMO2Compilation ? Ext.MO2CompilerSettings : Ext.CompilerSettings);
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public bool IsMO2Compilation { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
public CompilerConfigurationViewModel(DTOSerializer dtos, SettingsManager settingsManager)
|
public CompilerConfigurationViewModel(DTOSerializer dtos, SettingsManager settingsManager)
|
||||||
{
|
{
|
||||||
@ -73,24 +35,47 @@ public class CompilerConfigurationViewModel : ViewModelBase, IReceiverMarker
|
|||||||
Activator = new ViewModelActivator();
|
Activator = new ViewModelActivator();
|
||||||
|
|
||||||
AllGames = GameRegistry.Games.Values.ToArray();
|
AllGames = GameRegistry.Games.Values.ToArray();
|
||||||
|
|
||||||
StartCompilation = ReactiveCommand.Create(() => BeginCompilation().FireAndForget());
|
StartCompilation = ReactiveCommand.Create(() => BeginCompilation().FireAndForget());
|
||||||
|
|
||||||
OutputFolder = KnownFolders.EntryPoint;
|
OutputFolder = KnownFolders.EntryPoint;
|
||||||
|
|
||||||
this.WhenActivated(disposables =>
|
this.WhenActivated(disposables =>
|
||||||
{
|
{
|
||||||
LoadLastCompilation().FireAndForget();
|
LoadLastCompilation().FireAndForget();
|
||||||
this.WhenAnyValue(v => v.SettingsFile)
|
this.WhenAnyValue(v => v.SettingsFile)
|
||||||
.Subscribe( location =>
|
.Subscribe(location => { LoadNewSettingsFile(location).FireAndForget(); })
|
||||||
{
|
|
||||||
LoadNewSettingsFile(location).FireAndForget();
|
|
||||||
})
|
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Reactive] public string Title { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public AbsolutePath SettingsFile { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public AbsolutePath Downloads { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public GameMetaData BaseGame { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public AbsolutePath Source { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public AbsolutePath GamePath { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public string SelectedProfile { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public AbsolutePath OutputFolder { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public IEnumerable<GameMetaData> AllGames { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public ReactiveCommand<Unit, Unit> StartCompilation { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public IEnumerable<RelativePath> AlwaysEnabled { get; set; } = Array.Empty<RelativePath>();
|
||||||
|
|
||||||
|
public AbsolutePath SettingsOutputLocation => Source.Combine(Title)
|
||||||
|
.WithExtension(IsMO2Compilation ? Ext.MO2CompilerSettings : Ext.CompilerSettings);
|
||||||
|
|
||||||
|
[Reactive] public bool IsMO2Compilation { get; set; }
|
||||||
|
|
||||||
private async Task LoadNewSettingsFile(AbsolutePath location)
|
private async Task LoadNewSettingsFile(AbsolutePath location)
|
||||||
{
|
{
|
||||||
if (location == default) return;
|
if (location == default) return;
|
||||||
@ -108,7 +93,7 @@ public class CompilerConfigurationViewModel : ViewModelBase, IReceiverMarker
|
|||||||
var settings = GetSettings();
|
var settings = GetSettings();
|
||||||
await SaveSettingsFile();
|
await SaveSettingsFile();
|
||||||
await _settingsManager.Save("last_compilation", SettingsOutputLocation);
|
await _settingsManager.Save("last_compilation", SettingsOutputLocation);
|
||||||
|
|
||||||
MessageBus.Instance.Send(new StartCompilation(settings));
|
MessageBus.Instance.Send(new StartCompilation(settings));
|
||||||
MessageBus.Instance.Send(new NavigateTo(typeof(CompilationViewModel)));
|
MessageBus.Instance.Send(new NavigateTo(typeof(CompilationViewModel)));
|
||||||
}
|
}
|
||||||
@ -154,48 +139,44 @@ public class CompilerConfigurationViewModel : ViewModelBase, IReceiverMarker
|
|||||||
|
|
||||||
BaseGame = GameRegistry.GetByFuzzyName(general["gameName"].FromMO2Ini());
|
BaseGame = GameRegistry.GetByFuzzyName(general["gameName"].FromMO2Ini());
|
||||||
Source = mo2Folder;
|
Source = mo2Folder;
|
||||||
|
|
||||||
SelectedProfile = general["selected_profile"].FromMO2Ini();
|
SelectedProfile = general["selected_profile"].FromMO2Ini();
|
||||||
GamePath = general["gamePath"].FromMO2Ini().ToAbsolutePath();
|
GamePath = general["gamePath"].FromMO2Ini().ToAbsolutePath();
|
||||||
Title = SelectedProfile;
|
Title = SelectedProfile;
|
||||||
|
|
||||||
var settings = iniData["Settings"];
|
var settings = iniData["Settings"];
|
||||||
Downloads = settings["download_directory"].FromMO2Ini().ToAbsolutePath();
|
Downloads = settings["download_directory"].FromMO2Ini().ToAbsolutePath();
|
||||||
IsMO2Compilation = true;
|
IsMO2Compilation = true;
|
||||||
|
|
||||||
|
|
||||||
// Find Always Enabled mods
|
// Find Always Enabled mods
|
||||||
foreach (var modFolder in mo2Folder.Combine("mods").EnumerateDirectories())
|
foreach (var modFolder in mo2Folder.Combine("mods").EnumerateDirectories())
|
||||||
{
|
{
|
||||||
var iniFile = modFolder.Combine("meta.ini");
|
var iniFile = modFolder.Combine("meta.ini");
|
||||||
if (!iniFile.FileExists()) continue;
|
if (!iniFile.FileExists()) continue;
|
||||||
|
|
||||||
var data = iniFile.LoadIniFile();
|
var data = iniFile.LoadIniFile();
|
||||||
var generalModData = data["General"];
|
var generalModData = data["General"];
|
||||||
if ((generalModData["notes"]?.Contains("WABBAJACK_ALWAYS_ENABLE") ?? false) ||
|
if ((generalModData["notes"]?.Contains("WABBAJACK_ALWAYS_ENABLE") ?? false) ||
|
||||||
(generalModData["comments"]?.Contains("WABBAJACK_ALWAYS_ENABLE") ?? false))
|
(generalModData["comments"]?.Contains("WABBAJACK_ALWAYS_ENABLE") ?? false))
|
||||||
{
|
|
||||||
AlwaysEnabled = AlwaysEnabled.Append(modFolder.RelativeTo(mo2Folder)).ToArray();
|
AlwaysEnabled = AlwaysEnabled.Append(modFolder.RelativeTo(mo2Folder)).ToArray();
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mo2Folder.Depth > 1)
|
if (mo2Folder.Depth > 1)
|
||||||
OutputFolder = mo2Folder.Parent;
|
OutputFolder = mo2Folder.Parent;
|
||||||
|
|
||||||
await SaveSettingsFile();
|
await SaveSettingsFile();
|
||||||
SettingsFile = SettingsOutputLocation;
|
SettingsFile = SettingsOutputLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SaveSettingsFile()
|
private async Task SaveSettingsFile()
|
||||||
{
|
{
|
||||||
await using var st = SettingsOutputLocation.Open(FileMode.Create, FileAccess.Write, FileShare.None);
|
await using var st = SettingsOutputLocation.Open(FileMode.Create, FileAccess.Write, FileShare.None);
|
||||||
if (IsMO2Compilation)
|
if (IsMO2Compilation)
|
||||||
await JsonSerializer.SerializeAsync(st, (MO2CompilerSettings)GetSettings(), _dtos.Options);
|
await JsonSerializer.SerializeAsync(st, (MO2CompilerSettings) GetSettings(), _dtos.Options);
|
||||||
else
|
else
|
||||||
await JsonSerializer.SerializeAsync(st, GetSettings(), _dtos.Options);
|
await JsonSerializer.SerializeAsync(st, GetSettings(), _dtos.Options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,8 +6,8 @@
|
|||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Wabbajack.App.Screens.ErrorPageView">
|
x:Class="Wabbajack.App.Screens.ErrorPageView">
|
||||||
<Grid RowDefinitions="Auto, Auto, *">
|
<Grid RowDefinitions="Auto, Auto, *">
|
||||||
<TextBlock Grid.Row="0" x:Name="Prefix"></TextBlock>
|
<TextBlock Grid.Row="0" x:Name="Prefix" />
|
||||||
<TextBlock Grid.Row="1" x:Name="Message"></TextBlock>
|
<TextBlock Grid.Row="1" x:Name="Message" />
|
||||||
<controls:LogView Grid.Row="3"></controls:LogView>
|
<controls:LogView Grid.Row="3" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
@ -1,34 +1,31 @@
|
|||||||
using System;
|
using System;
|
||||||
using DynamicData.Kernel;
|
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using ReactiveUI.Fody.Helpers;
|
using ReactiveUI.Fody.Helpers;
|
||||||
using Wabbajack.App.Messages;
|
using Wabbajack.App.Messages;
|
||||||
using Wabbajack.App.ViewModels;
|
using Wabbajack.App.ViewModels;
|
||||||
|
|
||||||
namespace Wabbajack.App.Screens
|
namespace Wabbajack.App.Screens;
|
||||||
{
|
|
||||||
public class ErrorPageViewModel : ViewModelBase, IActivatableViewModel, IReceiver<Error>
|
|
||||||
{
|
|
||||||
[Reactive]
|
|
||||||
public string ShortMessage { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public string Prefix { get; set; }
|
|
||||||
|
|
||||||
public ErrorPageViewModel()
|
|
||||||
{
|
|
||||||
Activator = new ViewModelActivator();
|
|
||||||
}
|
|
||||||
public void Receive(Error val)
|
|
||||||
{
|
|
||||||
Prefix = val.Prefix;
|
|
||||||
ShortMessage = val.Exception.Message;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Display(string prefix, Exception ex)
|
public class ErrorPageViewModel : ViewModelBase, IActivatableViewModel, IReceiver<Error>
|
||||||
{
|
{
|
||||||
MessageBus.Instance.Send(new Error(prefix, ex));
|
public ErrorPageViewModel()
|
||||||
MessageBus.Instance.Send(new NavigateTo(typeof(ErrorPageViewModel)));
|
{
|
||||||
}
|
Activator = new ViewModelActivator();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Reactive] public string ShortMessage { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public string Prefix { get; set; }
|
||||||
|
|
||||||
|
public void Receive(Error val)
|
||||||
|
{
|
||||||
|
Prefix = val.Prefix;
|
||||||
|
ShortMessage = val.Exception.Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Display(string prefix, Exception ex)
|
||||||
|
{
|
||||||
|
MessageBus.Instance.Send(new Error(prefix, ex));
|
||||||
|
MessageBus.Instance.Send(new NavigateTo(typeof(ErrorPageViewModel)));
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -14,14 +14,14 @@
|
|||||||
<Image x:Name="ModListImage" Margin="0,0,0,0" Source="../Assets/Wabba_Mouth.png" />
|
<Image x:Name="ModListImage" Margin="0,0,0,0" Source="../Assets/Wabba_Mouth.png" />
|
||||||
</Viewbox>
|
</Viewbox>
|
||||||
<Grid Grid.Row="0" RowDefinitions="40, 40" HorizontalAlignment="Left" VerticalAlignment="Bottom">
|
<Grid Grid.Row="0" RowDefinitions="40, 40" HorizontalAlignment="Left" VerticalAlignment="Bottom">
|
||||||
<TextBlock x:Name="ModListName"></TextBlock>
|
<TextBlock x:Name="ModListName" />
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid Margin="40" RowDefinitions="40, 40, 40, *" ColumnDefinitions="100, *, 200" Grid.Row="1">
|
<Grid Margin="40" RowDefinitions="40, 40, 40, *" ColumnDefinitions="100, *, 200" Grid.Row="1">
|
||||||
<Label Grid.Column="0" Grid.Row="0" HorizontalAlignment="Right" VerticalAlignment="Center">ModList:</Label>
|
<Label Grid.Column="0" Grid.Row="0" HorizontalAlignment="Right" VerticalAlignment="Center">ModList:</Label>
|
||||||
<TextBox Grid.Column="1" Grid.Row="0" IsEnabled="False" Height="20" x:Name="ModList"></TextBox>
|
<TextBox Grid.Column="1" Grid.Row="0" IsEnabled="False" Height="20" x:Name="ModList" />
|
||||||
|
|
||||||
<Label Grid.Column="0" Grid.Row="1" HorizontalAlignment="Right" VerticalAlignment="Center">Location:</Label>
|
<Label Grid.Column="0" Grid.Row="1" HorizontalAlignment="Right" VerticalAlignment="Center">Location:</Label>
|
||||||
<TextBox Grid.Column="1" Grid.Row="1" IsEnabled="False" Height="20" x:Name="InstallPath"></TextBox>
|
<TextBox Grid.Column="1" Grid.Row="1" IsEnabled="False" Height="20" x:Name="InstallPath" />
|
||||||
|
|
||||||
<Grid Grid.Column="1" Grid.Row="3" Grid.ColumnDefinitions="*, *, *" HorizontalAlignment="Center">
|
<Grid Grid.Column="1" Grid.Row="3" Grid.ColumnDefinitions="*, *, *" HorizontalAlignment="Center">
|
||||||
<Button Grid.Column="0" x:Name="WebsiteButton">Website</Button>
|
<Button Grid.Column="0" x:Name="WebsiteButton">Website</Button>
|
||||||
@ -29,9 +29,8 @@
|
|||||||
<Button Grid.Column="2" x:Name="LocalFilesButton">Local Files</Button>
|
<Button Grid.Column="2" x:Name="LocalFilesButton">Local Files</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<controls:LargeIconButton x:Name="PlayGame" Margin="40, 0, 0, 0" Grid.Row="0" Grid.Column="2" Grid.RowSpan="4" Icon="PlayCircle" Text="Play">
|
<controls:LargeIconButton x:Name="PlayGame" Margin="40, 0, 0, 0" Grid.Row="0" Grid.Column="2"
|
||||||
|
Grid.RowSpan="4" Icon="PlayCircle" Text="Play" />
|
||||||
</controls:LargeIconButton>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
@ -1,33 +1,28 @@
|
|||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using Wabbajack.App.Views;
|
using Wabbajack.App.Views;
|
||||||
|
|
||||||
namespace Wabbajack.App.Screens
|
namespace Wabbajack.App.Screens;
|
||||||
|
|
||||||
|
public partial class LauncherView : ScreenBase<LauncherViewModel>
|
||||||
{
|
{
|
||||||
public partial class LauncherView : ScreenBase<LauncherViewModel>
|
public LauncherView()
|
||||||
{
|
{
|
||||||
public LauncherView()
|
InitializeComponent();
|
||||||
|
this.WhenActivated(disposables =>
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
this.OneWayBind(ViewModel, vm => vm.Image, view => view.ModListImage.Source)
|
||||||
this.WhenActivated(disposables =>
|
.DisposeWith(disposables);
|
||||||
{
|
|
||||||
this.OneWayBind(ViewModel, vm => vm.Image, view => view.ModListImage.Source)
|
|
||||||
.DisposeWith(disposables);
|
|
||||||
|
|
||||||
this.OneWayBind(ViewModel, vm => vm.Title, view => view.ModList.Text)
|
this.OneWayBind(ViewModel, vm => vm.Title, view => view.ModList.Text)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.OneWayBind(ViewModel, vm => vm.InstallFolder, view => view.InstallPath.Text,
|
this.OneWayBind(ViewModel, vm => vm.InstallFolder, view => view.InstallPath.Text,
|
||||||
v => v.ToString())
|
v => v.ToString())
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.BindCommand(ViewModel, vm => vm.PlayButton, view => view.PlayGame.Button)
|
|
||||||
.DisposeWith(disposables);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
this.BindCommand(ViewModel, vm => vm.PlayButton, view => view.PlayGame.Button)
|
||||||
|
.DisposeWith(disposables);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,8 +5,6 @@ using System.Reactive.Disposables;
|
|||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
using GameFinder.StoreHandlers.Origin.DTO;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using ReactiveUI.Fody.Helpers;
|
using ReactiveUI.Fody.Helpers;
|
||||||
@ -20,83 +18,68 @@ using Wabbajack.DTOs.SavedSettings;
|
|||||||
using Wabbajack.Paths;
|
using Wabbajack.Paths;
|
||||||
using Wabbajack.Paths.IO;
|
using Wabbajack.Paths.IO;
|
||||||
|
|
||||||
namespace Wabbajack.App.Screens
|
namespace Wabbajack.App.Screens;
|
||||||
|
|
||||||
|
public class LauncherViewModel : ViewModelBase, IActivatableViewModel, IReceiver<ConfigureLauncher>
|
||||||
{
|
{
|
||||||
public class LauncherViewModel : ViewModelBase, IActivatableViewModel, IReceiver<ConfigureLauncher>
|
private readonly ILogger<LauncherViewModel> _logger;
|
||||||
|
|
||||||
|
public ReactiveCommand<Unit, Unit> PlayButton;
|
||||||
|
|
||||||
|
public LauncherViewModel(ILogger<LauncherViewModel> logger, InstallationStateManager manager)
|
||||||
{
|
{
|
||||||
|
Activator = new ViewModelActivator();
|
||||||
|
PlayButton = ReactiveCommand.Create(() => { StartGame().FireAndForget(); });
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
[Reactive]
|
this.WhenActivated(disposables =>
|
||||||
public AbsolutePath InstallFolder { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public IBitmap Image { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public InstallationConfigurationSetting? Setting { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public string Title { get; set; }
|
|
||||||
|
|
||||||
public ReactiveCommand<Unit, Unit> PlayButton;
|
|
||||||
private readonly ILogger<LauncherViewModel> _logger;
|
|
||||||
|
|
||||||
public LauncherViewModel(ILogger<LauncherViewModel> logger, InstallationStateManager manager)
|
|
||||||
{
|
{
|
||||||
Activator = new ViewModelActivator();
|
this.WhenAnyValue(v => v.InstallFolder)
|
||||||
PlayButton = ReactiveCommand.Create(() =>
|
.SelectAsync(disposables, async folder => await manager.GetByInstallFolder(folder))
|
||||||
{
|
.ObserveOn(RxApp.MainThreadScheduler)
|
||||||
StartGame().FireAndForget();
|
.Where(v => v != null)
|
||||||
});
|
.BindTo(this, vm => vm.Setting)
|
||||||
_logger = logger;
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.WhenActivated(disposables =>
|
|
||||||
{
|
|
||||||
this.WhenAnyValue(v => v.InstallFolder)
|
|
||||||
.SelectAsync(disposables, async folder => await manager.GetByInstallFolder(folder))
|
|
||||||
.ObserveOn(RxApp.MainThreadScheduler)
|
|
||||||
.Where(v => v != null)
|
|
||||||
.BindTo(this, vm => vm.Setting)
|
|
||||||
.DisposeWith(disposables);
|
|
||||||
|
|
||||||
this.WhenAnyValue(v => v.Setting)
|
this.WhenAnyValue(v => v.Setting)
|
||||||
.Where(v => v != default)
|
.Where(v => v != default)
|
||||||
.Select(v => new Bitmap((v!.Image).ToString()))
|
.Select(v => new Bitmap(v!.Image.ToString()))
|
||||||
.BindTo(this, vm => vm.Image)
|
.BindTo(this, vm => vm.Image)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.WhenAnyValue(v => v.Setting)
|
this.WhenAnyValue(v => v.Setting)
|
||||||
.Where(v => v is { Metadata: { } })
|
.Where(v => v is {Metadata: { }})
|
||||||
.Select(v => $"{v!.Metadata!.Title} v{v!.Metadata.Version}")
|
.Select(v => $"{v!.Metadata!.Title} v{v!.Metadata.Version}")
|
||||||
.BindTo(this, vm => vm.Title)
|
.BindTo(this, vm => vm.Title)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
});
|
[Reactive] public AbsolutePath InstallFolder { get; set; }
|
||||||
}
|
|
||||||
|
|
||||||
private async Task StartGame()
|
[Reactive] public IBitmap Image { get; set; }
|
||||||
{
|
|
||||||
var mo2Path = InstallFolder.Combine("ModOrganizer.exe");
|
|
||||||
var gamePath = GameRegistry.Games.Values.Select(g => g.MainExecutable)
|
|
||||||
.Where(ge => ge != null)
|
|
||||||
.Select(ge => InstallFolder.Combine(ge!))
|
|
||||||
.FirstOrDefault(ge => ge.FileExists());
|
|
||||||
if (mo2Path.FileExists())
|
|
||||||
{
|
|
||||||
Process.Start(mo2Path.ToString());
|
|
||||||
}
|
|
||||||
else if (gamePath.FileExists())
|
|
||||||
{
|
|
||||||
Process.Start(gamePath.ToString());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogError("No way to launch game, no acceptable executable found");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Receive(ConfigureLauncher val)
|
[Reactive] public InstallationConfigurationSetting? Setting { get; set; }
|
||||||
{
|
|
||||||
InstallFolder = val.InstallFolder;
|
[Reactive] public string Title { get; set; }
|
||||||
}
|
|
||||||
|
public void Receive(ConfigureLauncher val)
|
||||||
|
{
|
||||||
|
InstallFolder = val.InstallFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StartGame()
|
||||||
|
{
|
||||||
|
var mo2Path = InstallFolder.Combine("ModOrganizer.exe");
|
||||||
|
var gamePath = GameRegistry.Games.Values.Select(g => g.MainExecutable)
|
||||||
|
.Where(ge => ge != null)
|
||||||
|
.Select(ge => InstallFolder.Combine(ge!))
|
||||||
|
.FirstOrDefault(ge => ge.FileExists());
|
||||||
|
if (mo2Path.FileExists())
|
||||||
|
Process.Start(mo2Path.ToString());
|
||||||
|
else if (gamePath.FileExists())
|
||||||
|
Process.Start(gamePath.ToString());
|
||||||
|
else
|
||||||
|
_logger.LogError("No way to launch game, no acceptable executable found");
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -6,6 +6,6 @@
|
|||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Wabbajack.App.Screens.LogScreenView">
|
x:Class="Wabbajack.App.Screens.LogScreenView">
|
||||||
<Grid RowDefinitions="*">
|
<Grid RowDefinitions="*">
|
||||||
<controls:LogView></controls:LogView>
|
<controls:LogView />
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
@ -1,9 +1,8 @@
|
|||||||
using ReactiveUI;
|
|
||||||
using Wabbajack.App.Views;
|
using Wabbajack.App.Views;
|
||||||
|
|
||||||
namespace Wabbajack.App.Screens;
|
namespace Wabbajack.App.Screens;
|
||||||
|
|
||||||
public partial class LogScreenView : ScreenBase<LogScreenViewModel>
|
public partial class LogScreenView : ScreenBase<LogScreenViewModel>
|
||||||
{
|
{
|
||||||
public LogScreenView()
|
public LogScreenView()
|
||||||
{
|
{
|
||||||
|
@ -1,8 +1,4 @@
|
|||||||
using System.Reactive;
|
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Input;
|
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using ReactiveUI.Fody.Helpers;
|
|
||||||
using Wabbajack.App.Utilities;
|
using Wabbajack.App.Utilities;
|
||||||
using Wabbajack.App.ViewModels;
|
using Wabbajack.App.ViewModels;
|
||||||
|
|
||||||
@ -11,11 +7,10 @@ namespace Wabbajack.App.Screens;
|
|||||||
public class LogScreenViewModel : ViewModelBase, IActivatableViewModel
|
public class LogScreenViewModel : ViewModelBase, IActivatableViewModel
|
||||||
{
|
{
|
||||||
private readonly LoggerProvider _provider;
|
private readonly LoggerProvider _provider;
|
||||||
|
|
||||||
public LogScreenViewModel(LoggerProvider provider)
|
public LogScreenViewModel(LoggerProvider provider)
|
||||||
{
|
{
|
||||||
_provider = provider;
|
_provider = provider;
|
||||||
Activator = new ViewModelActivator();
|
Activator = new ViewModelActivator();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -9,12 +9,12 @@
|
|||||||
<ItemsControl x:Name="Lists">
|
<ItemsControl x:Name="Lists">
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsControl.ItemsPanel>
|
||||||
<ItemsPanelTemplate>
|
<ItemsPanelTemplate>
|
||||||
<StackPanel></StackPanel>
|
<StackPanel />
|
||||||
</ItemsPanelTemplate>
|
</ItemsPanelTemplate>
|
||||||
</ItemsControl.ItemsPanel>
|
</ItemsControl.ItemsPanel>
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<controls:InstalledListView></controls:InstalledListView>
|
<controls:InstalledListView />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
|
@ -15,5 +15,4 @@ public partial class PlaySelectView : ScreenBase<PlaySelectViewModel>
|
|||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -8,22 +8,18 @@ using Wabbajack.App.Controls;
|
|||||||
using Wabbajack.App.Models;
|
using Wabbajack.App.Models;
|
||||||
using Wabbajack.App.ViewModels;
|
using Wabbajack.App.ViewModels;
|
||||||
using Wabbajack.Common;
|
using Wabbajack.Common;
|
||||||
using Wabbajack.DTOs.SavedSettings;
|
|
||||||
|
|
||||||
namespace Wabbajack.App.Screens;
|
namespace Wabbajack.App.Screens;
|
||||||
|
|
||||||
public class PlaySelectViewModel : ViewModelBase, IActivatableViewModel
|
public class PlaySelectViewModel : ViewModelBase, IActivatableViewModel
|
||||||
{
|
{
|
||||||
private readonly InstallationStateManager _manager;
|
private readonly InstallationStateManager _manager;
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public IEnumerable<InstalledListViewModel> Items { get; set; }
|
|
||||||
|
|
||||||
public PlaySelectViewModel(InstallationStateManager manager)
|
public PlaySelectViewModel(InstallationStateManager manager)
|
||||||
{
|
{
|
||||||
_manager = manager;
|
_manager = manager;
|
||||||
Activator = new ViewModelActivator();
|
Activator = new ViewModelActivator();
|
||||||
|
|
||||||
this.WhenActivated(disposables =>
|
this.WhenActivated(disposables =>
|
||||||
{
|
{
|
||||||
LoadAndSetItems().FireAndForget();
|
LoadAndSetItems().FireAndForget();
|
||||||
@ -31,10 +27,11 @@ public class PlaySelectViewModel : ViewModelBase, IActivatableViewModel
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Reactive] public IEnumerable<InstalledListViewModel> Items { get; set; }
|
||||||
|
|
||||||
public async Task LoadAndSetItems()
|
public async Task LoadAndSetItems()
|
||||||
{
|
{
|
||||||
var items = await _manager.GetAll();
|
var items = await _manager.GetAll();
|
||||||
Items = items.Settings.Select(a => new InstalledListViewModel(a)).ToArray();
|
Items = items.Settings.Select(a => new InstalledListViewModel(a)).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -9,43 +9,49 @@
|
|||||||
<Border x:Name="LoginBorder" Margin="5" BorderThickness="1">
|
<Border x:Name="LoginBorder" Margin="5" BorderThickness="1">
|
||||||
<Grid RowDefinitions="Auto, Auto, Auto, Auto" ColumnDefinitions="20, 100, Auto, Auto">
|
<Grid RowDefinitions="Auto, Auto, Auto, Auto" ColumnDefinitions="20, 100, Auto, Auto">
|
||||||
<TextBlock FontSize="20" Grid.ColumnSpan="4">Logins</TextBlock>
|
<TextBlock FontSize="20" Grid.ColumnSpan="4">Logins</TextBlock>
|
||||||
|
|
||||||
<Image Grid.Row="1" Grid.Column="0" Width="16" Height="16" Margin="4" Source="../Assets/Downloaders/nexus.ico" HorizontalAlignment="Right"></Image>
|
<Image Grid.Row="1" Grid.Column="0" Width="16" Height="16" Margin="4"
|
||||||
<TextBlock Grid.Row="1" Grid.Column="1" Text="Nexus" VerticalAlignment="Center" HorizontalAlignment="Left"></TextBlock>
|
Source="../Assets/Downloaders/nexus.ico" HorizontalAlignment="Right" />
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="1" Text="Nexus" VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Left" />
|
||||||
<Button Grid.Row="1" Grid.Column="2" x:Name="NexusLogIn">Log In</Button>
|
<Button Grid.Row="1" Grid.Column="2" x:Name="NexusLogIn">Log In</Button>
|
||||||
<Button Grid.Row="1" Grid.Column="3" x:Name="NexusLogOut">Log Out</Button>
|
<Button Grid.Row="1" Grid.Column="3" x:Name="NexusLogOut">Log Out</Button>
|
||||||
|
|
||||||
<Image Grid.Row="2" Grid.Column="0" Width="16" Height="16" Margin="4" Source="../Assets/Downloaders/loverslab.ico" HorizontalAlignment="Right"></Image>
|
<Image Grid.Row="2" Grid.Column="0" Width="16" Height="16" Margin="4"
|
||||||
<TextBlock Grid.Row="2" Grid.Column="1" Text="Lovers Lab" VerticalAlignment="Center" HorizontalAlignment="Left"></TextBlock>
|
Source="../Assets/Downloaders/loverslab.ico" HorizontalAlignment="Right" />
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="1" Text="Lovers Lab" VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Left" />
|
||||||
<Button Grid.Row="2" Grid.Column="2" x:Name="LoversLabLogIn">Log In</Button>
|
<Button Grid.Row="2" Grid.Column="2" x:Name="LoversLabLogIn">Log In</Button>
|
||||||
<Button Grid.Row="2" Grid.Column="3" x:Name="LoversLabLogOut">Log Out</Button>
|
<Button Grid.Row="2" Grid.Column="3" x:Name="LoversLabLogOut">Log Out</Button>
|
||||||
|
|
||||||
<Image Grid.Row="3" Grid.Column="0" Width="16" Height="16" Margin="4" Source="../Assets/Downloaders/vectorplexus.ico" HorizontalAlignment="Right"></Image>
|
<Image Grid.Row="3" Grid.Column="0" Width="16" Height="16" Margin="4"
|
||||||
<TextBlock Grid.Row="3" Grid.Column="1" Text="Vector Plexus" VerticalAlignment="Center" HorizontalAlignment="Left"></TextBlock>
|
Source="../Assets/Downloaders/vectorplexus.ico" HorizontalAlignment="Right" />
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="1" Text="Vector Plexus" VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Left" />
|
||||||
<Button Grid.Row="3" Grid.Column="2" x:Name="VectorPlexusLogIn">Log In</Button>
|
<Button Grid.Row="3" Grid.Column="2" x:Name="VectorPlexusLogIn">Log In</Button>
|
||||||
<Button Grid.Row="3" Grid.Column="3" x:Name="VectorPlexusLogOut">Log Out</Button>
|
<Button Grid.Row="3" Grid.Column="3" x:Name="VectorPlexusLogOut">Log Out</Button>
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Border x:Name="ResourcesBorder" Margin="5" BorderThickness="1">
|
<Border x:Name="ResourcesBorder" Margin="5" BorderThickness="1">
|
||||||
<Grid RowDefinitions="Auto, Auto">
|
<Grid RowDefinitions="Auto, Auto">
|
||||||
<TextBlock FontSize="20" Grid.ColumnSpan="4">Resource Limits</TextBlock>
|
<TextBlock FontSize="20" Grid.ColumnSpan="4">Resource Limits</TextBlock>
|
||||||
|
|
||||||
<ItemsControl Grid.Row="1" x:Name="ResourceList">
|
<ItemsControl Grid.Row="1" x:Name="ResourceList">
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsControl.ItemsPanel>
|
||||||
<ItemsPanelTemplate>
|
<ItemsPanelTemplate>
|
||||||
<StackPanel Orientation="Vertical"></StackPanel>
|
<StackPanel Orientation="Vertical" />
|
||||||
</ItemsPanelTemplate>
|
</ItemsPanelTemplate>
|
||||||
</ItemsControl.ItemsPanel>
|
</ItemsControl.ItemsPanel>
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<controls:ResourceView></controls:ResourceView>
|
<controls:ResourceView />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
</WrapPanel>
|
</WrapPanel>
|
||||||
</UserControl>
|
</UserControl>
|
@ -1,26 +1,22 @@
|
|||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using Wabbajack.App.ViewModels;
|
|
||||||
using Wabbajack.App.Views;
|
using Wabbajack.App.Views;
|
||||||
|
|
||||||
namespace Wabbajack.App.Screens
|
namespace Wabbajack.App.Screens;
|
||||||
|
|
||||||
|
public partial class SettingsView : ScreenBase<SettingsViewModel>
|
||||||
{
|
{
|
||||||
public partial class SettingsView : ScreenBase<SettingsViewModel>
|
public SettingsView()
|
||||||
{
|
{
|
||||||
public SettingsView()
|
InitializeComponent();
|
||||||
|
this.WhenActivated(disposables =>
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
this.BindCommand(ViewModel, vm => vm.NexusLogin, view => view.NexusLogIn)
|
||||||
this.WhenActivated(disposables =>
|
.DisposeWith(disposables);
|
||||||
{
|
this.BindCommand(ViewModel, vm => vm.NexusLogout, view => view.NexusLogOut)
|
||||||
this.BindCommand(ViewModel, vm => vm.NexusLogin, view => view.NexusLogIn)
|
.DisposeWith(disposables);
|
||||||
.DisposeWith(disposables);
|
this.OneWayBind(ViewModel, vm => vm.Resources, view => view.ResourceList.Items)
|
||||||
this.BindCommand(ViewModel, vm => vm.NexusLogout, view => view.NexusLogOut)
|
.DisposeWith(disposables);
|
||||||
.DisposeWith(disposables);
|
});
|
||||||
this.OneWayBind(ViewModel, vm => vm.Resources, view => view.ResourceList.Items)
|
|
||||||
.DisposeWith(disposables);
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -15,56 +15,51 @@ using Wabbajack.Paths.IO;
|
|||||||
using Wabbajack.RateLimiter;
|
using Wabbajack.RateLimiter;
|
||||||
using Wabbajack.Services.OSIntegrated.TokenProviders;
|
using Wabbajack.Services.OSIntegrated.TokenProviders;
|
||||||
|
|
||||||
namespace Wabbajack.App.Screens
|
namespace Wabbajack.App.Screens;
|
||||||
|
|
||||||
|
public class SettingsViewModel : ViewModelBase, IReceiverMarker
|
||||||
{
|
{
|
||||||
public class SettingsViewModel : ViewModelBase, IReceiverMarker
|
private readonly Subject<AbsolutePath> _fileSystemEvents = new();
|
||||||
|
private readonly ILogger<SettingsViewModel> _logger;
|
||||||
|
public readonly IEnumerable<ResourceViewModel> Resources;
|
||||||
|
|
||||||
|
public SettingsViewModel(ILogger<SettingsViewModel> logger, Configuration configuration,
|
||||||
|
NexusApiTokenProvider nexusProvider, IEnumerable<IResource> resources)
|
||||||
{
|
{
|
||||||
private readonly ILogger<SettingsViewModel> _logger;
|
_logger = logger;
|
||||||
|
Resources = resources.Select(r => new ResourceViewModel(r)).ToArray();
|
||||||
public ReactiveCommand<Unit, Unit> NexusLogin { get; set; }
|
Activator = new ViewModelActivator();
|
||||||
public ReactiveCommand<Unit, Unit> NexusLogout { get; set; }
|
|
||||||
|
|
||||||
public FileSystemWatcher Watcher { get; set; }
|
|
||||||
|
|
||||||
private readonly Subject<AbsolutePath> _fileSystemEvents = new();
|
this.WhenActivated(disposables =>
|
||||||
public readonly IEnumerable<ResourceViewModel> Resources;
|
|
||||||
|
|
||||||
public SettingsViewModel(ILogger<SettingsViewModel> logger, Configuration configuration, NexusApiTokenProvider nexusProvider, IEnumerable<IResource> resources)
|
|
||||||
{
|
{
|
||||||
_logger = logger;
|
configuration.EncryptedDataLocation.CreateDirectory();
|
||||||
Resources = resources.Select(r => new ResourceViewModel(r)).ToArray();
|
Watcher = new FileSystemWatcher(configuration.EncryptedDataLocation.ToString());
|
||||||
Activator = new ViewModelActivator();
|
Watcher.DisposeWith(disposables);
|
||||||
|
Watcher.Created += Pulse;
|
||||||
this.WhenActivated(disposables =>
|
Watcher.Deleted += Pulse;
|
||||||
{
|
Watcher.Renamed += Pulse;
|
||||||
configuration.EncryptedDataLocation.CreateDirectory();
|
Watcher.Changed += Pulse;
|
||||||
Watcher = new FileSystemWatcher(configuration.EncryptedDataLocation.ToString());
|
|
||||||
Watcher.DisposeWith(disposables);
|
|
||||||
Watcher.Created += Pulse;
|
|
||||||
Watcher.Deleted += Pulse;
|
|
||||||
Watcher.Renamed += Pulse;
|
|
||||||
Watcher.Changed += Pulse;
|
|
||||||
|
|
||||||
Watcher.EnableRaisingEvents = true;
|
|
||||||
|
|
||||||
var haveNexusToken = this._fileSystemEvents
|
Watcher.EnableRaisingEvents = true;
|
||||||
.StartWith(AbsolutePath.Empty)
|
|
||||||
.Select(_ => nexusProvider.HaveToken());
|
|
||||||
|
|
||||||
NexusLogin = ReactiveCommand.Create(() =>
|
var haveNexusToken = _fileSystemEvents
|
||||||
{
|
.StartWith(AbsolutePath.Empty)
|
||||||
MessageBus.Instance.Send(new NavigateTo(typeof(NexusLoginViewModel)));
|
.Select(_ => nexusProvider.HaveToken());
|
||||||
}, haveNexusToken.Select(x => !x));
|
|
||||||
NexusLogout = ReactiveCommand.Create(nexusProvider.DeleteToken, haveNexusToken.Select(x => x));
|
|
||||||
|
|
||||||
|
|
||||||
|
NexusLogin =
|
||||||
|
ReactiveCommand.Create(() => { MessageBus.Instance.Send(new NavigateTo(typeof(NexusLoginViewModel))); },
|
||||||
|
haveNexusToken.Select(x => !x));
|
||||||
|
NexusLogout = ReactiveCommand.Create(nexusProvider.DeleteToken, haveNexusToken.Select(x => x));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
});
|
public ReactiveCommand<Unit, Unit> NexusLogin { get; set; }
|
||||||
}
|
public ReactiveCommand<Unit, Unit> NexusLogout { get; set; }
|
||||||
|
|
||||||
private void Pulse(object sender, FileSystemEventArgs e)
|
public FileSystemWatcher Watcher { get; set; }
|
||||||
{
|
|
||||||
_fileSystemEvents.OnNext(e.FullPath?.ToAbsolutePath() ?? default);
|
private void Pulse(object sender, FileSystemEventArgs e)
|
||||||
}
|
{
|
||||||
|
_fileSystemEvents.OnNext(e.FullPath?.ToAbsolutePath() ?? default);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,18 +7,26 @@
|
|||||||
x:Class="Wabbajack.App.Views.StandardInstallationView">
|
x:Class="Wabbajack.App.Views.StandardInstallationView">
|
||||||
<Grid RowDefinitions="40, 5, 5, *, 40">
|
<Grid RowDefinitions="40, 5, 5, *, 40">
|
||||||
<TextBlock Grid.Row="0" x:Name="StatusText" FontSize="20" FontWeight="Bold">[20/30] Installing Files</TextBlock>
|
<TextBlock Grid.Row="0" x:Name="StatusText" FontSize="20" FontWeight="Bold">[20/30] Installing Files</TextBlock>
|
||||||
<ProgressBar Grid.Row="1" x:Name="StepsProgress" Maximum="1000" Value="40"></ProgressBar>
|
<ProgressBar Grid.Row="1" x:Name="StepsProgress" Maximum="1000" Value="40" />
|
||||||
<ProgressBar Grid.Row="2" x:Name="StepProgress" Maximum="10000" Value="30"></ProgressBar>
|
<ProgressBar Grid.Row="2" x:Name="StepProgress" Maximum="10000" Value="30" />
|
||||||
<Viewbox Grid.Row="3" HorizontalAlignment="Center"
|
<Viewbox Grid.Row="3" HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Stretch="Uniform">
|
Stretch="Uniform">
|
||||||
<Image x:Name="SlideImage"></Image>
|
<Image x:Name="SlideImage" />
|
||||||
</Viewbox>
|
</Viewbox>
|
||||||
<Grid Grid.Row="4" HorizontalAlignment="Center" ColumnDefinitions="40, 40, 40, 40">
|
<Grid Grid.Row="4" HorizontalAlignment="Center" ColumnDefinitions="40, 40, 40, 40">
|
||||||
<Button Grid.Column="0" x:Name="PrevSlide"><i:MaterialIcon Kind="ArrowLeft"></i:MaterialIcon></Button>
|
<Button Grid.Column="0" x:Name="PrevSlide">
|
||||||
<Button Grid.Column="1" x:Name="PauseSlides"><i:MaterialIcon Kind="Pause"></i:MaterialIcon></Button>
|
<i:MaterialIcon Kind="ArrowLeft" />
|
||||||
<Button Grid.Column="2" x:Name="PlaySlides"><i:MaterialIcon Kind="PlayArrow"></i:MaterialIcon></Button>
|
</Button>
|
||||||
<Button Grid.Column="3" x:Name="NextSlide"><i:MaterialIcon Kind="ArrowRight"></i:MaterialIcon></Button>
|
<Button Grid.Column="1" x:Name="PauseSlides">
|
||||||
|
<i:MaterialIcon Kind="Pause" />
|
||||||
|
</Button>
|
||||||
|
<Button Grid.Column="2" x:Name="PlaySlides">
|
||||||
|
<i:MaterialIcon Kind="PlayArrow" />
|
||||||
|
</Button>
|
||||||
|
<Button Grid.Column="3" x:Name="NextSlide">
|
||||||
|
<i:MaterialIcon Kind="ArrowRight" />
|
||||||
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
@ -2,42 +2,39 @@ using System.Reactive.Disposables;
|
|||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using Wabbajack.App.ViewModels;
|
using Wabbajack.App.ViewModels;
|
||||||
|
|
||||||
namespace Wabbajack.App.Views
|
namespace Wabbajack.App.Views;
|
||||||
|
|
||||||
|
public partial class StandardInstallationView : ScreenBase<StandardInstallationViewModel>
|
||||||
{
|
{
|
||||||
public partial class StandardInstallationView : ScreenBase<StandardInstallationViewModel>
|
public StandardInstallationView()
|
||||||
{
|
{
|
||||||
public StandardInstallationView()
|
InitializeComponent();
|
||||||
|
|
||||||
|
this.WhenActivated(disposables =>
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
this.Bind(ViewModel, vm => vm.Slide.Image, view => view.SlideImage.Source)
|
||||||
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.WhenActivated(disposables =>
|
this.BindCommand(ViewModel, vm => vm.NextCommand, view => view.NextSlide)
|
||||||
{
|
.DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.Slide.Image, view => view.SlideImage.Source)
|
|
||||||
.DisposeWith(disposables);
|
|
||||||
|
|
||||||
this.BindCommand(ViewModel, vm => vm.NextCommand, view => view.NextSlide)
|
this.BindCommand(ViewModel, vm => vm.PrevCommand, view => view.PrevSlide)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.BindCommand(ViewModel, vm => vm.PrevCommand, view => view.PrevSlide)
|
|
||||||
.DisposeWith(disposables);
|
|
||||||
|
|
||||||
this.BindCommand(ViewModel, vm => vm.PauseCommand, view => view.PauseSlides)
|
|
||||||
.DisposeWith(disposables);
|
|
||||||
|
|
||||||
this.BindCommand(ViewModel, vm => vm.PlayCommand, view => view.PlaySlides)
|
|
||||||
.DisposeWith(disposables);
|
|
||||||
|
|
||||||
this.OneWayBind(ViewModel, vm => vm.StatusText, view => view.StatusText.Text)
|
this.BindCommand(ViewModel, vm => vm.PauseCommand, view => view.PauseSlides)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.OneWayBind(ViewModel, vm => vm.StepsProgress, view => view.StepsProgress.Value, p => p.Value * 1000)
|
this.BindCommand(ViewModel, vm => vm.PlayCommand, view => view.PlaySlides)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.OneWayBind(ViewModel, vm => vm.StepProgress, view => view.StepProgress.Value, p => p.Value * 10000)
|
|
||||||
.DisposeWith(disposables);
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
this.OneWayBind(ViewModel, vm => vm.StatusText, view => view.StatusText.Text)
|
||||||
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
|
this.OneWayBind(ViewModel, vm => vm.StepsProgress, view => view.StepsProgress.Value, p => p.Value * 1000)
|
||||||
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
|
this.OneWayBind(ViewModel, vm => vm.StepProgress, view => view.StepProgress.Value, p => p.Value * 10000)
|
||||||
|
.DisposeWith(disposables);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -8,7 +8,6 @@ using System.Reactive.Disposables;
|
|||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Media;
|
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@ -29,206 +28,193 @@ using Wabbajack.Installer;
|
|||||||
using Wabbajack.Paths.IO;
|
using Wabbajack.Paths.IO;
|
||||||
using Wabbajack.RateLimiter;
|
using Wabbajack.RateLimiter;
|
||||||
|
|
||||||
namespace Wabbajack.App.ViewModels
|
namespace Wabbajack.App.ViewModels;
|
||||||
|
|
||||||
|
public class StandardInstallationViewModel : ViewModelBase, IReceiver<StartInstallation>
|
||||||
{
|
{
|
||||||
public class StandardInstallationViewModel : ViewModelBase, IReceiver<StartInstallation>
|
private readonly DTOSerializer _dtos;
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly InstallationStateManager _installStateManager;
|
||||||
|
private readonly GameLocator _locator;
|
||||||
|
private readonly ILogger<StandardInstallationViewModel> _logger;
|
||||||
|
private readonly IServiceProvider _provider;
|
||||||
|
private InstallerConfiguration _config;
|
||||||
|
private int _currentSlideIndex;
|
||||||
|
private StandardInstaller _installer;
|
||||||
|
private IServiceScope _scope;
|
||||||
|
private SlideViewModel[] _slides = Array.Empty<SlideViewModel>();
|
||||||
|
private Timer _slideTimer;
|
||||||
|
|
||||||
|
public StandardInstallationViewModel(ILogger<StandardInstallationViewModel> logger, IServiceProvider provider,
|
||||||
|
GameLocator locator, DTOSerializer dtos,
|
||||||
|
HttpClient httpClient, InstallationStateManager manager)
|
||||||
{
|
{
|
||||||
private readonly IServiceProvider _provider;
|
_provider = provider;
|
||||||
private readonly GameLocator _locator;
|
_locator = locator;
|
||||||
private IServiceScope _scope;
|
_logger = logger;
|
||||||
private InstallerConfiguration _config;
|
_dtos = dtos;
|
||||||
private StandardInstaller _installer;
|
_httpClient = httpClient;
|
||||||
private readonly ILogger<StandardInstallationViewModel> _logger;
|
_installStateManager = manager;
|
||||||
private readonly DTOSerializer _dtos;
|
Activator = new ViewModelActivator();
|
||||||
private SlideViewModel[] _slides = Array.Empty<SlideViewModel>();
|
|
||||||
private readonly HttpClient _httpClient;
|
|
||||||
private Timer _slideTimer;
|
|
||||||
private int _currentSlideIndex;
|
|
||||||
private readonly InstallationStateManager _installStateManager;
|
|
||||||
|
|
||||||
[Reactive]
|
this.WhenActivated(disposables =>
|
||||||
public SlideViewModel Slide { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public ReactiveCommand<Unit,Unit> NextCommand { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public ReactiveCommand<Unit, Unit> PrevCommand { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public ReactiveCommand<Unit, bool> PauseCommand { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public ReactiveCommand<Unit, bool> PlayCommand { get; set; }
|
|
||||||
|
|
||||||
[Reactive] public bool IsPlaying { get; set; } = true;
|
|
||||||
|
|
||||||
[Reactive] public string StatusText { get; set; } = "";
|
|
||||||
[Reactive] public Percent StepsProgress { get; set; } = Percent.Zero;
|
|
||||||
[Reactive] public Percent StepProgress { get; set; } = Percent.Zero;
|
|
||||||
|
|
||||||
public StandardInstallationViewModel(ILogger<StandardInstallationViewModel> logger, IServiceProvider provider, GameLocator locator, DTOSerializer dtos,
|
|
||||||
HttpClient httpClient, InstallationStateManager manager)
|
|
||||||
{
|
{
|
||||||
_provider = provider;
|
_slideTimer = new Timer(_ =>
|
||||||
_locator = locator;
|
{
|
||||||
_logger = logger;
|
if (IsPlaying) NextSlide(1);
|
||||||
_dtos = dtos;
|
}, null, TimeSpan.FromSeconds(0.1), TimeSpan.FromSeconds(5));
|
||||||
_httpClient = httpClient;
|
|
||||||
_installStateManager = manager;
|
|
||||||
Activator = new ViewModelActivator();
|
|
||||||
|
|
||||||
this.WhenActivated(disposables => {
|
_currentSlideIndex = 0;
|
||||||
_slideTimer = new Timer(_ =>
|
_slideTimer.DisposeWith(disposables);
|
||||||
{
|
|
||||||
if (IsPlaying) NextSlide(1);
|
|
||||||
}, null, TimeSpan.FromSeconds(0.1), TimeSpan.FromSeconds(5));
|
|
||||||
|
|
||||||
_currentSlideIndex = 0;
|
|
||||||
_slideTimer.DisposeWith(disposables);
|
|
||||||
|
|
||||||
NextCommand = ReactiveCommand.Create(() => NextSlide(1))
|
NextCommand = ReactiveCommand.Create(() => NextSlide(1))
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
PrevCommand = ReactiveCommand.Create(() => NextSlide(-1))
|
PrevCommand = ReactiveCommand.Create(() => NextSlide(-1))
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
PauseCommand = ReactiveCommand.Create(() => IsPlaying = false,
|
PauseCommand = ReactiveCommand.Create(() => IsPlaying = false,
|
||||||
this.ObservableForProperty(vm => vm.IsPlaying, skipInitial:false)
|
this.ObservableForProperty(vm => vm.IsPlaying, skipInitial: false)
|
||||||
.Select(v => v.Value))
|
.Select(v => v.Value))
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
PlayCommand = ReactiveCommand.Create(() => IsPlaying = true,
|
PlayCommand = ReactiveCommand.Create(() => IsPlaying = true,
|
||||||
this.ObservableForProperty(vm => vm.IsPlaying, skipInitial:false)
|
this.ObservableForProperty(vm => vm.IsPlaying, skipInitial: false)
|
||||||
.Select(v => !v.Value))
|
.Select(v => !v.Value))
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Reactive] public SlideViewModel Slide { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public ReactiveCommand<Unit, Unit> NextCommand { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public ReactiveCommand<Unit, Unit> PrevCommand { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public ReactiveCommand<Unit, bool> PauseCommand { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public ReactiveCommand<Unit, bool> PlayCommand { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public bool IsPlaying { get; set; } = true;
|
||||||
|
|
||||||
|
[Reactive] public string StatusText { get; set; } = "";
|
||||||
|
[Reactive] public Percent StepsProgress { get; set; } = Percent.Zero;
|
||||||
|
[Reactive] public Percent StepProgress { get; set; } = Percent.Zero;
|
||||||
|
|
||||||
|
public void Receive(StartInstallation msg)
|
||||||
|
{
|
||||||
|
Install(msg).FireAndForget();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void NextSlide(int direction)
|
||||||
|
{
|
||||||
|
if (_slides.Length == 0) return;
|
||||||
|
_currentSlideIndex = InSlideRange(_currentSlideIndex + direction);
|
||||||
|
|
||||||
|
var thisSlide = _slides[_currentSlideIndex];
|
||||||
|
|
||||||
|
if (thisSlide.Image == null)
|
||||||
|
thisSlide.PreCache(_httpClient).FireAndForget();
|
||||||
|
|
||||||
|
// Cache the next image
|
||||||
|
_slides[InSlideRange(_currentSlideIndex + 1)].PreCache(_httpClient).FireAndForget();
|
||||||
|
|
||||||
|
var prevSlide = _slides[InSlideRange(_currentSlideIndex - 2)];
|
||||||
|
//if (prevSlide.Image != null)
|
||||||
|
// prevSlide.Image = null;
|
||||||
|
|
||||||
|
Dispatcher.UIThread.InvokeAsync(() => { Slide = thisSlide; });
|
||||||
|
}
|
||||||
|
|
||||||
|
private int InSlideRange(int i)
|
||||||
|
{
|
||||||
|
while (!(i >= 0 && i <= _slides.Length))
|
||||||
|
{
|
||||||
|
if (i >= _slides.Length) i -= _slides.Length;
|
||||||
|
if (i < 0) i += _slides.Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Install(StartInstallation msg)
|
||||||
|
{
|
||||||
|
_scope = _provider.CreateScope();
|
||||||
|
_config = _provider.GetService<InstallerConfiguration>()!;
|
||||||
|
_config.Downloads = msg.Download;
|
||||||
|
_config.Install = msg.Install;
|
||||||
|
_config.ModlistArchive = msg.ModListPath;
|
||||||
|
_config.Metadata = msg.Metadata;
|
||||||
|
|
||||||
|
_logger.LogInformation("Loading ModList Data");
|
||||||
|
_config.ModList = await StandardInstaller.LoadFromFile(_dtos, msg.ModListPath);
|
||||||
|
_config.Game = _config.ModList.GameType;
|
||||||
|
|
||||||
private void NextSlide(int direction)
|
_slides = _config.ModList.Archives.Select(a => a.State).OfType<IMetaState>()
|
||||||
|
.Select(m => new SlideViewModel {MetaState = m})
|
||||||
|
.Shuffle()
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
_slides[1].PreCache(_httpClient).FireAndForget();
|
||||||
|
Slide = _slides[1];
|
||||||
|
|
||||||
|
if (_config.GameFolder == default)
|
||||||
{
|
{
|
||||||
if (_slides.Length == 0) return;
|
if (!_locator.TryFindLocation(_config.Game, out var found))
|
||||||
_currentSlideIndex = InSlideRange(_currentSlideIndex + direction);
|
|
||||||
|
|
||||||
var thisSlide = _slides[_currentSlideIndex];
|
|
||||||
|
|
||||||
if (thisSlide.Image == null)
|
|
||||||
thisSlide.PreCache(_httpClient).FireAndForget();
|
|
||||||
|
|
||||||
// Cache the next image
|
|
||||||
_slides[InSlideRange(_currentSlideIndex + 1)].PreCache(_httpClient).FireAndForget();
|
|
||||||
|
|
||||||
var prevSlide = _slides[InSlideRange(_currentSlideIndex - 2)];
|
|
||||||
//if (prevSlide.Image != null)
|
|
||||||
// prevSlide.Image = null;
|
|
||||||
|
|
||||||
Dispatcher.UIThread.InvokeAsync(() =>
|
|
||||||
{
|
{
|
||||||
Slide = thisSlide;
|
_logger.LogCritical("Game {game} is not installed on this system",
|
||||||
});
|
_config.Game.MetaData().HumanFriendlyGameName);
|
||||||
|
throw new Exception("Game not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
_config.GameFolder = found;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int InSlideRange(int i)
|
_installer = _provider.GetService<StandardInstaller>()!;
|
||||||
{
|
|
||||||
while (!(i >= 0 && i <= _slides.Length))
|
|
||||||
{
|
|
||||||
if (i >= _slides.Length) i -= _slides.Length;
|
|
||||||
if (i < 0) i += _slides.Length;
|
|
||||||
}
|
|
||||||
|
|
||||||
return i;
|
_installer.OnStatusUpdate = async update =>
|
||||||
|
{
|
||||||
|
Trace.TraceInformation("Update....");
|
||||||
|
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
StatusText = update.StatusText;
|
||||||
|
StepsProgress = update.StepsProgress;
|
||||||
|
StepProgress = update.StepProgress;
|
||||||
|
}, DispatcherPriority.Background);
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogInformation("Installer created, starting the installation process");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _installer.Begin(CancellationToken.None);
|
||||||
|
if (!result) throw new Exception("Installation failed");
|
||||||
|
|
||||||
|
if (result) await SaveConfigAndContinue(_config);
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
public void Receive(StartInstallation msg)
|
|
||||||
{
|
{
|
||||||
|
ErrorPageViewModel.Display("During installation", ex);
|
||||||
Install(msg).FireAndForget();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task Install(StartInstallation msg)
|
|
||||||
{
|
|
||||||
_scope = _provider.CreateScope();
|
|
||||||
_config = _provider.GetService<InstallerConfiguration>()!;
|
|
||||||
_config.Downloads = msg.Download;
|
|
||||||
_config.Install = msg.Install;
|
|
||||||
_config.ModlistArchive = msg.ModListPath;
|
|
||||||
_config.Metadata = msg.Metadata;
|
|
||||||
|
|
||||||
_logger.LogInformation("Loading ModList Data");
|
|
||||||
_config.ModList = await StandardInstaller.LoadFromFile(_dtos, msg.ModListPath);
|
|
||||||
_config.Game = _config.ModList.GameType;
|
|
||||||
|
|
||||||
_slides = _config.ModList.Archives.Select(a => a.State).OfType<IMetaState>()
|
|
||||||
.Select(m => new SlideViewModel { MetaState = m })
|
|
||||||
.Shuffle()
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
_slides[1].PreCache(_httpClient).FireAndForget();
|
|
||||||
Slide = _slides[1];
|
|
||||||
|
|
||||||
if (_config.GameFolder == default)
|
|
||||||
{
|
|
||||||
if (!_locator.TryFindLocation(_config.Game, out var found))
|
|
||||||
{
|
|
||||||
_logger.LogCritical("Game {game} is not installed on this system",
|
|
||||||
_config.Game.MetaData().HumanFriendlyGameName);
|
|
||||||
throw new Exception("Game not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
_config.GameFolder = found;
|
|
||||||
}
|
|
||||||
|
|
||||||
_installer = _provider.GetService<StandardInstaller>()!;
|
|
||||||
|
|
||||||
_installer.OnStatusUpdate = async update =>
|
|
||||||
{
|
|
||||||
Trace.TraceInformation("Update....");
|
|
||||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
|
||||||
{
|
|
||||||
StatusText = update.StatusText;
|
|
||||||
StepsProgress = update.StepsProgress;
|
|
||||||
StepProgress = update.StepProgress;
|
|
||||||
}, DispatcherPriority.Background);
|
|
||||||
};
|
|
||||||
|
|
||||||
_logger.LogInformation("Installer created, starting the installation process");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = await _installer.Begin(CancellationToken.None);
|
|
||||||
if (!result) throw new Exception("Installation failed");
|
|
||||||
|
|
||||||
if (result)
|
|
||||||
{
|
|
||||||
await SaveConfigAndContinue(_config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
ErrorPageViewModel.Display("During installation", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SaveConfigAndContinue(InstallerConfiguration config)
|
|
||||||
{
|
|
||||||
var path = config.Install.Combine("modlist-image.png");
|
|
||||||
{
|
|
||||||
var image = await ModListUtilities.GetModListImageStream(config.ModlistArchive);
|
|
||||||
await using var os = path.Open(FileMode.Create, FileAccess.Write);
|
|
||||||
await image.CopyToAsync(os);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _installStateManager.SetLastState(new InstallationConfigurationSetting
|
|
||||||
{
|
|
||||||
Downloads = config.Downloads,
|
|
||||||
Install = config.Install,
|
|
||||||
Metadata = config.Metadata,
|
|
||||||
ModList = config.ModlistArchive,
|
|
||||||
Image = path
|
|
||||||
});
|
|
||||||
|
|
||||||
MessageBus.Instance.Send(new ConfigureLauncher(config.Install));
|
|
||||||
MessageBus.Instance.Send(new NavigateTo(typeof(LauncherViewModel)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task SaveConfigAndContinue(InstallerConfiguration config)
|
||||||
|
{
|
||||||
|
var path = config.Install.Combine("modlist-image.png");
|
||||||
|
{
|
||||||
|
var image = await ModListUtilities.GetModListImageStream(config.ModlistArchive);
|
||||||
|
await using var os = path.Open(FileMode.Create, FileAccess.Write);
|
||||||
|
await image.CopyToAsync(os);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _installStateManager.SetLastState(new InstallationConfigurationSetting
|
||||||
|
{
|
||||||
|
Downloads = config.Downloads,
|
||||||
|
Install = config.Install,
|
||||||
|
Metadata = config.Metadata,
|
||||||
|
ModList = config.ModlistArchive,
|
||||||
|
Image = path
|
||||||
|
});
|
||||||
|
|
||||||
|
MessageBus.Instance.Send(new ConfigureLauncher(config.Install));
|
||||||
|
MessageBus.Instance.Send(new NavigateTo(typeof(LauncherViewModel)));
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Reactive.Disposables;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
@ -20,127 +19,123 @@ using Wabbajack.Downloaders.Interfaces;
|
|||||||
using Wabbajack.DTOs;
|
using Wabbajack.DTOs;
|
||||||
using Wabbajack.DTOs.DownloadStates;
|
using Wabbajack.DTOs.DownloadStates;
|
||||||
using Wabbajack.DTOs.JsonConverters;
|
using Wabbajack.DTOs.JsonConverters;
|
||||||
using Wabbajack.Networking.NexusApi;
|
|
||||||
using Wabbajack.Paths;
|
|
||||||
using Wabbajack.Paths.IO;
|
using Wabbajack.Paths.IO;
|
||||||
using Wabbajack.Services.OSIntegrated;
|
using Wabbajack.Services.OSIntegrated;
|
||||||
using SettingsView = Wabbajack.App.Screens.SettingsView;
|
|
||||||
|
|
||||||
namespace Wabbajack.App
|
namespace Wabbajack.App;
|
||||||
|
|
||||||
|
public static class ServiceExtensions
|
||||||
{
|
{
|
||||||
public static class ServiceExtensions
|
private const int messagePumpDelay = 10;
|
||||||
|
|
||||||
|
|
||||||
|
private static CefAppImpl app;
|
||||||
|
private static Timer messagePump;
|
||||||
|
|
||||||
|
public static IServiceCollection AddAppServices(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddAppServices(this IServiceCollection services)
|
services.AddAllSingleton<ILoggerProvider, LoggerProvider>();
|
||||||
|
services.AddSingleton<MessageBus>();
|
||||||
|
services.AddSingleton<MainWindow>();
|
||||||
|
services.AddSingleton<BrowseViewModel>();
|
||||||
|
services.AddTransient<BrowseItemViewModel>();
|
||||||
|
services.AddTransient<LogViewModel>();
|
||||||
|
|
||||||
|
services.AddTransient<InstalledListViewModel>();
|
||||||
|
|
||||||
|
services.AddDTOConverters();
|
||||||
|
services.AddDTOSerializer();
|
||||||
|
services.AddSingleton<ModeSelectionViewModel>();
|
||||||
|
services.AddTransient<FileSelectionBoxViewModel>();
|
||||||
|
services.AddSingleton<IScreenView, ErrorPageView>();
|
||||||
|
services.AddSingleton<IScreenView, LogScreenView>();
|
||||||
|
services.AddSingleton<IScreenView, ModeSelectionView>();
|
||||||
|
services.AddSingleton<IScreenView, InstallConfigurationView>();
|
||||||
|
services.AddSingleton<IScreenView, CompilerConfigurationView>();
|
||||||
|
services.AddSingleton<IScreenView, StandardInstallationView>();
|
||||||
|
services.AddSingleton<IScreenView, CompilationView>();
|
||||||
|
services.AddSingleton<IScreenView, SettingsView>();
|
||||||
|
services.AddSingleton<IScreenView, BrowseView>();
|
||||||
|
services.AddSingleton<IScreenView, LauncherView>();
|
||||||
|
services.AddSingleton<IScreenView, PlaySelectView>();
|
||||||
|
|
||||||
|
services.AddSingleton<InstallationStateManager>();
|
||||||
|
services.AddSingleton<HttpClient>();
|
||||||
|
|
||||||
|
services.AddSingleton<LogScreenViewModel>();
|
||||||
|
services.AddSingleton<PlaySelectViewModel>();
|
||||||
|
services.AddAllSingleton<IReceiverMarker, ErrorPageViewModel>();
|
||||||
|
services.AddAllSingleton<IReceiverMarker, StandardInstallationViewModel>();
|
||||||
|
services.AddAllSingleton<IReceiverMarker, InstallConfigurationViewModel>();
|
||||||
|
services.AddAllSingleton<IReceiverMarker, CompilerConfigurationViewModel>();
|
||||||
|
services.AddAllSingleton<IReceiverMarker, MainWindowViewModel>();
|
||||||
|
services.AddAllSingleton<IReceiverMarker, SettingsViewModel>();
|
||||||
|
services.AddAllSingleton<IReceiverMarker, NexusLoginViewModel>();
|
||||||
|
services.AddAllSingleton<IReceiverMarker, LoversLabOAuthLoginViewModel>();
|
||||||
|
services.AddAllSingleton<IReceiverMarker, VectorPlexusOAuthLoginViewModel>();
|
||||||
|
services.AddAllSingleton<IReceiverMarker, CompilationViewModel>();
|
||||||
|
services.AddAllSingleton<IReceiverMarker, LauncherViewModel>();
|
||||||
|
|
||||||
|
// Services
|
||||||
|
services.AddAllSingleton<IDownloader, IDownloader<Manual>, ManualDownloader>();
|
||||||
|
|
||||||
|
var resources = KnownFolders.EntryPoint;
|
||||||
|
services.AddSingleton(s => new CefSettings
|
||||||
{
|
{
|
||||||
services.AddAllSingleton<ILoggerProvider, LoggerProvider>();
|
NoSandbox = true,
|
||||||
services.AddSingleton<MessageBus>();
|
PersistSessionCookies = true,
|
||||||
services.AddSingleton<MainWindow>();
|
MultiThreadedMessageLoop = false,
|
||||||
services.AddSingleton<BrowseViewModel>();
|
WindowlessRenderingEnabled = true,
|
||||||
services.AddTransient<BrowseItemViewModel>();
|
ExternalMessagePump = true,
|
||||||
services.AddTransient<LogViewModel>();
|
LocalesDirPath = resources.Combine("locales").ToString(),
|
||||||
|
ResourcesDirPath = resources.ToString(),
|
||||||
|
UserAgent = "",
|
||||||
|
CachePath = KnownFolders.WabbajackAppLocal.Combine("cef_cache").ToString()
|
||||||
|
});
|
||||||
|
|
||||||
services.AddTransient<InstalledListViewModel>();
|
services.AddSingleton(s => new Configuration
|
||||||
|
|
||||||
services.AddDTOConverters();
|
|
||||||
services.AddDTOSerializer();
|
|
||||||
services.AddSingleton<ModeSelectionViewModel>();
|
|
||||||
services.AddTransient<FileSelectionBoxViewModel>();
|
|
||||||
services.AddSingleton<IScreenView, ErrorPageView>();
|
|
||||||
services.AddSingleton<IScreenView, LogScreenView>();
|
|
||||||
services.AddSingleton<IScreenView, ModeSelectionView>();
|
|
||||||
services.AddSingleton<IScreenView, InstallConfigurationView>();
|
|
||||||
services.AddSingleton<IScreenView, CompilerConfigurationView>();
|
|
||||||
services.AddSingleton<IScreenView, StandardInstallationView>();
|
|
||||||
services.AddSingleton<IScreenView, CompilationView>();
|
|
||||||
services.AddSingleton<IScreenView, SettingsView>();
|
|
||||||
services.AddSingleton<IScreenView, BrowseView>();
|
|
||||||
services.AddSingleton<IScreenView, LauncherView>();
|
|
||||||
services.AddSingleton<IScreenView, PlaySelectView>();
|
|
||||||
|
|
||||||
services.AddSingleton<InstallationStateManager>();
|
|
||||||
services.AddSingleton<HttpClient>();
|
|
||||||
|
|
||||||
services.AddSingleton<LogScreenViewModel>();
|
|
||||||
services.AddSingleton<PlaySelectViewModel>();
|
|
||||||
services.AddAllSingleton<IReceiverMarker, ErrorPageViewModel>();
|
|
||||||
services.AddAllSingleton<IReceiverMarker, StandardInstallationViewModel>();
|
|
||||||
services.AddAllSingleton<IReceiverMarker, InstallConfigurationViewModel>();
|
|
||||||
services.AddAllSingleton<IReceiverMarker, CompilerConfigurationViewModel>();
|
|
||||||
services.AddAllSingleton<IReceiverMarker, MainWindowViewModel>();
|
|
||||||
services.AddAllSingleton<IReceiverMarker, SettingsViewModel>();
|
|
||||||
services.AddAllSingleton<IReceiverMarker, NexusLoginViewModel>();
|
|
||||||
services.AddAllSingleton<IReceiverMarker, LoversLabOAuthLoginViewModel>();
|
|
||||||
services.AddAllSingleton<IReceiverMarker, VectorPlexusOAuthLoginViewModel>();
|
|
||||||
services.AddAllSingleton<IReceiverMarker, CompilationViewModel>();
|
|
||||||
services.AddAllSingleton<IReceiverMarker, LauncherViewModel>();
|
|
||||||
|
|
||||||
// Services
|
|
||||||
services.AddAllSingleton<IDownloader, IDownloader<Manual>, ManualDownloader>();
|
|
||||||
|
|
||||||
var resources = KnownFolders.EntryPoint;
|
|
||||||
services.AddSingleton(s => new CefSettings()
|
|
||||||
{
|
|
||||||
NoSandbox = true,
|
|
||||||
PersistSessionCookies = true,
|
|
||||||
MultiThreadedMessageLoop = false,
|
|
||||||
WindowlessRenderingEnabled = true,
|
|
||||||
ExternalMessagePump = true,
|
|
||||||
LocalesDirPath = resources.Combine("locales").ToString(),
|
|
||||||
ResourcesDirPath = resources.ToString(),
|
|
||||||
UserAgent = "",
|
|
||||||
CachePath = KnownFolders.WabbajackAppLocal.Combine("cef_cache").ToString()
|
|
||||||
});
|
|
||||||
|
|
||||||
services.AddSingleton(s => new Configuration
|
|
||||||
{
|
|
||||||
EncryptedDataLocation = KnownFolders.WabbajackAppLocal.Combine("encrypted"),
|
|
||||||
ModListsDownloadLocation = KnownFolders.EntryPoint.Combine("downloaded_mod_lists"),
|
|
||||||
SavedSettingsLocation = KnownFolders.WabbajackAppLocal.Combine("saved_settings"),
|
|
||||||
LogLocation = KnownFolders.EntryPoint.Combine("logs")
|
|
||||||
});
|
|
||||||
|
|
||||||
services.AddSingleton<SettingsManager>();
|
|
||||||
|
|
||||||
services.AddSingleton(s =>
|
|
||||||
{
|
|
||||||
App.FrameworkInitialized += App_FrameworkInitialized;
|
|
||||||
|
|
||||||
var app = new CefAppImpl();
|
|
||||||
app.ScheduleMessagePumpWorkCallback = OnScheduleMessagePumpWork;
|
|
||||||
|
|
||||||
app.CefProcessMessageReceived += App_CefProcessMessageReceived;
|
|
||||||
app.Initialize(resources.ToString(), s.GetService<CefSettings>());
|
|
||||||
|
|
||||||
|
|
||||||
return app;
|
|
||||||
});
|
|
||||||
|
|
||||||
services.AddOSIntegrated();
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async void OnScheduleMessagePumpWork(long delayMs)
|
|
||||||
{
|
{
|
||||||
await Task.Delay((int)delayMs);
|
EncryptedDataLocation = KnownFolders.WabbajackAppLocal.Combine("encrypted"),
|
||||||
Dispatcher.UIThread.Post(CefApi.DoMessageLoopWork);
|
ModListsDownloadLocation = KnownFolders.EntryPoint.Combine("downloaded_mod_lists"),
|
||||||
}
|
SavedSettingsLocation = KnownFolders.WabbajackAppLocal.Combine("saved_settings"),
|
||||||
|
LogLocation = KnownFolders.EntryPoint.Combine("logs")
|
||||||
|
});
|
||||||
|
|
||||||
private static void App_CefProcessMessageReceived(object? sender, CefProcessMessageReceivedEventArgs e)
|
services.AddSingleton<SettingsManager>();
|
||||||
|
|
||||||
|
services.AddSingleton(s =>
|
||||||
{
|
{
|
||||||
var msg = e.Name;
|
App.FrameworkInitialized += App_FrameworkInitialized;
|
||||||
|
|
||||||
}
|
var app = new CefAppImpl();
|
||||||
|
app.ScheduleMessagePumpWorkCallback = OnScheduleMessagePumpWork;
|
||||||
|
|
||||||
|
app.CefProcessMessageReceived += App_CefProcessMessageReceived;
|
||||||
|
app.Initialize(resources.ToString(), s.GetService<CefSettings>());
|
||||||
|
|
||||||
|
|
||||||
private static CefAppImpl app;
|
return app;
|
||||||
private static Timer messagePump;
|
});
|
||||||
private const int messagePumpDelay = 10;
|
|
||||||
private static void App_FrameworkInitialized(object? sender, EventArgs e)
|
services.AddOSIntegrated();
|
||||||
{
|
return services;
|
||||||
if (CefNetApplication.Instance.UsesExternalMessageLoop)
|
}
|
||||||
{
|
|
||||||
messagePump = new Timer(_ => Dispatcher.UIThread.Post(CefApi.DoMessageLoopWork), null, messagePumpDelay, messagePumpDelay);
|
private static async void OnScheduleMessagePumpWork(long delayMs)
|
||||||
}
|
{
|
||||||
}
|
await Task.Delay((int) delayMs);
|
||||||
|
Dispatcher.UIThread.Post(CefApi.DoMessageLoopWork);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void App_CefProcessMessageReceived(object? sender, CefProcessMessageReceivedEventArgs e)
|
||||||
|
{
|
||||||
|
var msg = e.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void App_FrameworkInitialized(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (CefNetApplication.Instance.UsesExternalMessageLoop)
|
||||||
|
messagePump = new Timer(_ => Dispatcher.UIThread.Post(CefApi.DoMessageLoopWork), null, messagePumpDelay,
|
||||||
|
messagePumpDelay);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -11,44 +11,42 @@ using Wabbajack.Hashing.xxHash64;
|
|||||||
using Wabbajack.Paths;
|
using Wabbajack.Paths;
|
||||||
using Wabbajack.RateLimiter;
|
using Wabbajack.RateLimiter;
|
||||||
|
|
||||||
namespace Wabbajack.App.Services
|
namespace Wabbajack.App.Services;
|
||||||
|
|
||||||
|
public class ManualDownloader : ADownloader<Manual>
|
||||||
{
|
{
|
||||||
public class ManualDownloader : ADownloader<Manual>
|
public override Priority Priority { get; }
|
||||||
|
|
||||||
|
public override Task<Hash> Download(Archive archive, Manual state, AbsolutePath destination, IJob job,
|
||||||
|
CancellationToken token)
|
||||||
{
|
{
|
||||||
public override Task<Hash> Download(Archive archive, Manual state, AbsolutePath destination, IJob job, CancellationToken token)
|
throw new NotImplementedException();
|
||||||
{
|
}
|
||||||
throw new System.NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Task<bool> Prepare()
|
public override Task<bool> Prepare()
|
||||||
{
|
{
|
||||||
return Task.FromResult(true);
|
return Task.FromResult(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool IsAllowed(ServerAllowList allowList, IDownloadState state)
|
public override bool IsAllowed(ServerAllowList allowList, IDownloadState state)
|
||||||
{
|
{
|
||||||
var manual = (Manual)state;
|
var manual = (Manual) state;
|
||||||
return allowList.AllowedPrefixes.Any(p => manual.Url.ToString().StartsWith(p));
|
return allowList.AllowedPrefixes.Any(p => manual.Url.ToString().StartsWith(p));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IDownloadState? Resolve(IReadOnlyDictionary<string, string> iniData)
|
public override IDownloadState? Resolve(IReadOnlyDictionary<string, string> iniData)
|
||||||
{
|
{
|
||||||
if (iniData.TryGetValue("manualURL", out var manual))
|
if (iniData.TryGetValue("manualURL", out var manual)) return new Manual {Url = new Uri(manual)};
|
||||||
{
|
return null;
|
||||||
return new Manual { Url = new Uri(manual) };
|
}
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Priority Priority { get; }
|
public override Task<bool> Verify(Archive archive, Manual archiveState, IJob job, CancellationToken token)
|
||||||
public override Task<bool> Verify(Archive archive, Manual archiveState, IJob job, CancellationToken token)
|
{
|
||||||
{
|
return Task.FromResult(true);
|
||||||
return Task.FromResult(true);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public override IEnumerable<string> MetaIni(Archive a, Manual state)
|
public override IEnumerable<string> MetaIni(Archive a, Manual state)
|
||||||
{
|
{
|
||||||
return new[] { $"manualURL={state.Url}" };
|
return new[] {$"manualURL={state.Url}"};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,72 +2,70 @@ using System;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using CefNet;
|
using CefNet;
|
||||||
|
|
||||||
namespace Wabbajack.App.Utilities
|
namespace Wabbajack.App.Utilities;
|
||||||
|
|
||||||
|
internal class CefAppImpl : CefNetApplication
|
||||||
{
|
{
|
||||||
class CefAppImpl : CefNetApplication
|
public Action<long> ScheduleMessagePumpWorkCallback { get; set; }
|
||||||
|
|
||||||
|
protected override void OnBeforeCommandLineProcessing(string processType, CefCommandLine commandLine)
|
||||||
{
|
{
|
||||||
protected override void OnBeforeCommandLineProcessing(string processType, CefCommandLine commandLine)
|
base.OnBeforeCommandLineProcessing(processType, commandLine);
|
||||||
|
|
||||||
|
Console.WriteLine("ChromiumWebBrowser_OnBeforeCommandLineProcessing");
|
||||||
|
Console.WriteLine(commandLine.CommandLineString);
|
||||||
|
|
||||||
|
//commandLine.AppendSwitchWithValue("proxy-server", "127.0.0.1:8888");
|
||||||
|
|
||||||
|
|
||||||
|
commandLine.AppendSwitchWithValue("remote-debugging-port", "9222");
|
||||||
|
|
||||||
|
//enable-devtools-experiments
|
||||||
|
commandLine.AppendSwitch("enable-devtools-experiments");
|
||||||
|
|
||||||
|
//e.CommandLine.AppendSwitchWithValue("user-agent", "Mozilla/5.0 (Windows 10.0) WebKa/" + DateTime.UtcNow.Ticks);
|
||||||
|
|
||||||
|
//("force-device-scale-factor", "1");
|
||||||
|
|
||||||
|
//commandLine.AppendSwitch("disable-gpu");
|
||||||
|
//commandLine.AppendSwitch("disable-gpu-compositing");
|
||||||
|
//commandLine.AppendSwitch("disable-gpu-vsync");
|
||||||
|
|
||||||
|
commandLine.AppendSwitch("enable-begin-frame-scheduling");
|
||||||
|
commandLine.AppendSwitch("enable-media-stream");
|
||||||
|
|
||||||
|
commandLine.AppendSwitchWithValue("enable-blink-features", "CSSPseudoHas");
|
||||||
|
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
{
|
{
|
||||||
base.OnBeforeCommandLineProcessing(processType, commandLine);
|
commandLine.AppendSwitch("no-zygote");
|
||||||
|
commandLine.AppendSwitch("no-sandbox");
|
||||||
Console.WriteLine("ChromiumWebBrowser_OnBeforeCommandLineProcessing");
|
|
||||||
Console.WriteLine(commandLine.CommandLineString);
|
|
||||||
|
|
||||||
//commandLine.AppendSwitchWithValue("proxy-server", "127.0.0.1:8888");
|
|
||||||
|
|
||||||
|
|
||||||
commandLine.AppendSwitchWithValue("remote-debugging-port", "9222");
|
|
||||||
|
|
||||||
//enable-devtools-experiments
|
|
||||||
commandLine.AppendSwitch("enable-devtools-experiments");
|
|
||||||
|
|
||||||
//e.CommandLine.AppendSwitchWithValue("user-agent", "Mozilla/5.0 (Windows 10.0) WebKa/" + DateTime.UtcNow.Ticks);
|
|
||||||
|
|
||||||
//("force-device-scale-factor", "1");
|
|
||||||
|
|
||||||
//commandLine.AppendSwitch("disable-gpu");
|
|
||||||
//commandLine.AppendSwitch("disable-gpu-compositing");
|
|
||||||
//commandLine.AppendSwitch("disable-gpu-vsync");
|
|
||||||
|
|
||||||
commandLine.AppendSwitch("enable-begin-frame-scheduling");
|
|
||||||
commandLine.AppendSwitch("enable-media-stream");
|
|
||||||
|
|
||||||
commandLine.AppendSwitchWithValue("enable-blink-features", "CSSPseudoHas");
|
|
||||||
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
|
||||||
{
|
|
||||||
commandLine.AppendSwitch("no-zygote");
|
|
||||||
commandLine.AppendSwitch("no-sandbox");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnContextCreated(CefBrowser browser, CefFrame frame, CefV8Context context)
|
protected override void OnContextCreated(CefBrowser browser, CefFrame frame, CefV8Context context)
|
||||||
{
|
{
|
||||||
base.OnContextCreated(browser, frame, context);
|
base.OnContextCreated(browser, frame, context);
|
||||||
frame.ExecuteJavaScript(@"
|
frame.ExecuteJavaScript(@"
|
||||||
{
|
{
|
||||||
const newProto = navigator.__proto__;
|
const newProto = navigator.__proto__;
|
||||||
delete newProto.webdriver;
|
delete newProto.webdriver;
|
||||||
navigator.__proto__ = newProto;
|
navigator.__proto__ = newProto;
|
||||||
}", frame.Url, 0);
|
}", frame.Url, 0);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
protected override void OnCefProcessMessageReceived(CefProcessMessageReceivedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnCefProcessMessageReceived(e);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnCefProcessMessageReceived(CefProcessMessageReceivedEventArgs e)
|
protected override CefRenderProcessHandler GetRenderProcessHandler()
|
||||||
{
|
{
|
||||||
base.OnCefProcessMessageReceived(e);
|
return base.GetRenderProcessHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Action<long> ScheduleMessagePumpWorkCallback { get; set; }
|
protected override void OnScheduleMessagePumpWork(long delayMs)
|
||||||
|
{
|
||||||
protected override CefRenderProcessHandler GetRenderProcessHandler()
|
ScheduleMessagePumpWorkCallback(delayMs);
|
||||||
{
|
|
||||||
return base.GetRenderProcessHandler();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnScheduleMessagePumpWork(long delayMs)
|
|
||||||
{
|
|
||||||
ScheduleMessagePumpWorkCallback(delayMs);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -8,7 +8,6 @@ using System.Text;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Primitives;
|
|
||||||
using Wabbajack.Paths;
|
using Wabbajack.Paths;
|
||||||
using Wabbajack.Paths.IO;
|
using Wabbajack.Paths.IO;
|
||||||
|
|
||||||
@ -16,36 +15,33 @@ namespace Wabbajack.App.Utilities;
|
|||||||
|
|
||||||
public class LoggerProvider : ILoggerProvider
|
public class LoggerProvider : ILoggerProvider
|
||||||
{
|
{
|
||||||
private Subject<ILogMessage> _messages = new();
|
|
||||||
public IObservable<ILogMessage> Messages => _messages;
|
|
||||||
|
|
||||||
private long _messageId = 0;
|
|
||||||
private SourceCache<ILogMessage, long> _messageLog = new(m => m.MessageId);
|
|
||||||
|
|
||||||
public readonly ReadOnlyObservableCollection<ILogMessage> _messagesFiltered;
|
|
||||||
private readonly CompositeDisposable _disposables;
|
|
||||||
private readonly Configuration _configuration;
|
|
||||||
private readonly DateTime _startupTime;
|
|
||||||
private readonly RelativePath _appName;
|
private readonly RelativePath _appName;
|
||||||
public AbsolutePath LogPath { get; }
|
private readonly Configuration _configuration;
|
||||||
|
private readonly CompositeDisposable _disposables;
|
||||||
private readonly Stream _logFile;
|
private readonly Stream _logFile;
|
||||||
private readonly StreamWriter _logStream;
|
private readonly StreamWriter _logStream;
|
||||||
public ReadOnlyObservableCollection<ILogMessage> MessageLog => _messagesFiltered;
|
|
||||||
|
public readonly ReadOnlyObservableCollection<ILogMessage> _messagesFiltered;
|
||||||
|
private readonly DateTime _startupTime;
|
||||||
|
|
||||||
|
private long _messageId;
|
||||||
|
private readonly SourceCache<ILogMessage, long> _messageLog = new(m => m.MessageId);
|
||||||
|
private readonly Subject<ILogMessage> _messages = new();
|
||||||
|
|
||||||
public LoggerProvider(Configuration configuration)
|
public LoggerProvider(Configuration configuration)
|
||||||
{
|
{
|
||||||
_startupTime = DateTime.UtcNow;
|
_startupTime = DateTime.UtcNow;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_configuration.LogLocation.CreateDirectory();
|
_configuration.LogLocation.CreateDirectory();
|
||||||
|
|
||||||
_disposables = new CompositeDisposable();
|
_disposables = new CompositeDisposable();
|
||||||
|
|
||||||
Messages.Subscribe(m => _messageLog.AddOrUpdate(m))
|
Messages.Subscribe(m => _messageLog.AddOrUpdate(m))
|
||||||
.DisposeWith(_disposables);
|
.DisposeWith(_disposables);
|
||||||
|
|
||||||
Messages.Subscribe(m => LogToFile(m))
|
Messages.Subscribe(m => LogToFile(m))
|
||||||
.DisposeWith(_disposables);
|
.DisposeWith(_disposables);
|
||||||
|
|
||||||
_messageLog.Connect()
|
_messageLog.Connect()
|
||||||
.Bind(out _messagesFiltered)
|
.Bind(out _messagesFiltered)
|
||||||
.Subscribe()
|
.Subscribe()
|
||||||
@ -55,11 +51,24 @@ public class LoggerProvider : ILoggerProvider
|
|||||||
|
|
||||||
_appName = typeof(LoggerProvider).Assembly.Location.ToAbsolutePath().FileName;
|
_appName = typeof(LoggerProvider).Assembly.Location.ToAbsolutePath().FileName;
|
||||||
LogPath = _configuration.LogLocation.Combine($"{_appName}.current.log");
|
LogPath = _configuration.LogLocation.Combine($"{_appName}.current.log");
|
||||||
_logFile = LogPath.Open(FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
|
_logFile = LogPath.Open(FileMode.Append, FileAccess.Write);
|
||||||
_logFile.DisposeWith(_disposables);
|
_logFile.DisposeWith(_disposables);
|
||||||
|
|
||||||
_logStream = new StreamWriter(_logFile, Encoding.UTF8);
|
_logStream = new StreamWriter(_logFile, Encoding.UTF8);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IObservable<ILogMessage> Messages => _messages;
|
||||||
|
public AbsolutePath LogPath { get; }
|
||||||
|
public ReadOnlyObservableCollection<ILogMessage> MessageLog => _messagesFiltered;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_disposables.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ILogger CreateLogger(string categoryName)
|
||||||
|
{
|
||||||
|
return new Logger(this, categoryName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LogToFile(ILogMessage logMessage)
|
private void LogToFile(ILogMessage logMessage)
|
||||||
@ -77,21 +86,11 @@ public class LoggerProvider : ILoggerProvider
|
|||||||
return Interlocked.Increment(ref _messageId);
|
return Interlocked.Increment(ref _messageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_disposables.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ILogger CreateLogger(string categoryName)
|
|
||||||
{
|
|
||||||
return new Logger(this, categoryName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Logger : ILogger
|
public class Logger : ILogger
|
||||||
{
|
{
|
||||||
|
private readonly string _categoryName;
|
||||||
private readonly LoggerProvider _provider;
|
private readonly LoggerProvider _provider;
|
||||||
private ImmutableList<object> Scopes = ImmutableList<object>.Empty;
|
private ImmutableList<object> Scopes = ImmutableList<object>.Empty;
|
||||||
private readonly string _categoryName;
|
|
||||||
|
|
||||||
public Logger(LoggerProvider provider, string categoryName)
|
public Logger(LoggerProvider provider, string categoryName)
|
||||||
{
|
{
|
||||||
@ -99,9 +98,11 @@ public class LoggerProvider : ILoggerProvider
|
|||||||
_provider = provider;
|
_provider = provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
|
||||||
|
Func<TState, Exception?, string> formatter)
|
||||||
{
|
{
|
||||||
_provider._messages.OnNext(new LogMessage<TState>(DateTime.UtcNow, _provider.NextMessageId(), logLevel, eventId, state, exception, formatter));
|
_provider._messages.OnNext(new LogMessage<TState>(DateTime.UtcNow, _provider.NextMessageId(), logLevel,
|
||||||
|
eventId, state, exception, formatter));
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsEnabled(LogLevel logLevel)
|
public bool IsEnabled(LogLevel logLevel)
|
||||||
@ -125,10 +126,11 @@ public class LoggerProvider : ILoggerProvider
|
|||||||
string LongMessage { get; }
|
string LongMessage { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
record LogMessage<TState>(DateTime TimeStamp, long MessageId, LogLevel LogLevel, EventId EventId, TState State, Exception? Exception, Func<TState, Exception?, string> Formatter) : ILogMessage
|
private record LogMessage<TState>(DateTime TimeStamp, long MessageId, LogLevel LogLevel, EventId EventId,
|
||||||
|
TState State, Exception? Exception, Func<TState, Exception?, string> Formatter) : ILogMessage
|
||||||
{
|
{
|
||||||
public string ShortMessage => Formatter(State, Exception);
|
public string ShortMessage => Formatter(State, Exception);
|
||||||
|
|
||||||
public string LongMessage
|
public string LongMessage
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
@ -5,17 +5,16 @@ using Wabbajack.Common;
|
|||||||
using Wabbajack.Paths;
|
using Wabbajack.Paths;
|
||||||
using Wabbajack.Paths.IO;
|
using Wabbajack.Paths.IO;
|
||||||
|
|
||||||
namespace Wabbajack.App.Utilities
|
namespace Wabbajack.App.Utilities;
|
||||||
|
|
||||||
|
public class ModListUtilities
|
||||||
{
|
{
|
||||||
public class ModListUtilities
|
public static async Task<MemoryStream> GetModListImageStream(AbsolutePath modList)
|
||||||
{
|
{
|
||||||
public static async Task<MemoryStream> GetModListImageStream(AbsolutePath modList)
|
await using var fs = modList.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
{
|
using var ar = new ZipArchive(fs, ZipArchiveMode.Read);
|
||||||
await using var fs = modList.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
|
var entry = ar.GetEntry("modlist-image.png");
|
||||||
using var ar = new ZipArchive(fs, ZipArchiveMode.Read);
|
await using var stream = entry!.Open();
|
||||||
var entry = ar.GetEntry("modlist-image.png");
|
return new MemoryStream(await stream.ReadAllAsync());
|
||||||
await using var stream = entry!.Open();
|
|
||||||
return new MemoryStream(await stream.ReadAllAsync());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,13 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using Wabbajack.Common;
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace Wabbajack.App
|
namespace Wabbajack.App;
|
||||||
|
|
||||||
|
public static class Utils
|
||||||
{
|
{
|
||||||
public static class Utils
|
public static void OpenWebsiteInExternalBrowser(Uri uri)
|
||||||
{
|
{
|
||||||
public static void OpenWebsiteInExternalBrowser(Uri uri)
|
Process.Start(uri.ToString());
|
||||||
{
|
|
||||||
System.Diagnostics.Process.Start(uri.ToString());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,30 +3,24 @@ using Avalonia.Controls;
|
|||||||
using Avalonia.Controls.Templates;
|
using Avalonia.Controls.Templates;
|
||||||
using Wabbajack.App.ViewModels;
|
using Wabbajack.App.ViewModels;
|
||||||
|
|
||||||
namespace Wabbajack.App
|
namespace Wabbajack.App;
|
||||||
|
|
||||||
|
public class ViewLocator : IDataTemplate
|
||||||
{
|
{
|
||||||
public class ViewLocator : IDataTemplate
|
public bool SupportsRecycling => false;
|
||||||
|
|
||||||
|
public IControl Build(object data)
|
||||||
{
|
{
|
||||||
public bool SupportsRecycling => false;
|
var name = data.GetType().FullName!.Replace("ViewModel", "View");
|
||||||
|
var type = Type.GetType(name);
|
||||||
|
|
||||||
public IControl Build(object data)
|
if (type != null)
|
||||||
{
|
return (Control) Activator.CreateInstance(type)!;
|
||||||
var name = data.GetType().FullName!.Replace("ViewModel", "View");
|
return new TextBlock {Text = "Not Found: " + name};
|
||||||
var type = Type.GetType(name);
|
}
|
||||||
|
|
||||||
if (type != null)
|
public bool Match(object data)
|
||||||
{
|
{
|
||||||
return (Control)Activator.CreateInstance(type)!;
|
return data is ViewModelBase;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return new TextBlock { Text = "Not Found: " + name };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Match(object data)
|
|
||||||
{
|
|
||||||
return data is ViewModelBase;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,36 +1,29 @@
|
|||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CefNet;
|
|
||||||
using CefNet.Avalonia;
|
using CefNet.Avalonia;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using ReactiveUI.Fody.Helpers;
|
using ReactiveUI.Fody.Helpers;
|
||||||
using Wabbajack.App.Messages;
|
using Wabbajack.App.Messages;
|
||||||
using Wabbajack.Common;
|
|
||||||
|
|
||||||
namespace Wabbajack.App.ViewModels
|
namespace Wabbajack.App.ViewModels;
|
||||||
|
|
||||||
|
public abstract class GuidedWebViewModel : ViewModelBase, IReceiverMarker
|
||||||
{
|
{
|
||||||
public abstract class GuidedWebViewModel : ViewModelBase, IReceiverMarker
|
protected ILogger _logger;
|
||||||
|
|
||||||
|
public GuidedWebViewModel(ILogger logger)
|
||||||
{
|
{
|
||||||
protected ILogger _logger;
|
_logger = logger;
|
||||||
|
Activator = new ViewModelActivator();
|
||||||
|
|
||||||
[Reactive]
|
this.WhenActivated(disposables => { Disposable.Empty.DisposeWith(disposables); });
|
||||||
public string Instructions { get; set; }
|
|
||||||
|
|
||||||
public GuidedWebViewModel(ILogger logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
Activator = new ViewModelActivator();
|
|
||||||
|
|
||||||
this.WhenActivated(disposables =>
|
|
||||||
{
|
|
||||||
Disposable.Empty.DisposeWith(disposables);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public WebView Browser { get; set; }
|
|
||||||
|
|
||||||
public abstract Task Run(CancellationToken token);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Reactive] public string Instructions { get; set; }
|
||||||
|
|
||||||
|
public WebView Browser { get; set; }
|
||||||
|
|
||||||
|
public abstract Task Run(CancellationToken token);
|
||||||
}
|
}
|
@ -1,5 +1,3 @@
|
|||||||
using System.IO;
|
|
||||||
using System.IO.Compression;
|
|
||||||
using System.Reactive;
|
using System.Reactive;
|
||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
@ -21,121 +19,107 @@ using Wabbajack.Installer;
|
|||||||
using Wabbajack.Paths;
|
using Wabbajack.Paths;
|
||||||
using Wabbajack.Paths.IO;
|
using Wabbajack.Paths.IO;
|
||||||
|
|
||||||
namespace Wabbajack.App.ViewModels
|
namespace Wabbajack.App.ViewModels;
|
||||||
|
|
||||||
|
public class InstallConfigurationViewModel : ViewModelBase, IActivatableViewModel, IReceiver<StartInstallConfiguration>
|
||||||
{
|
{
|
||||||
public class InstallConfigurationViewModel : ViewModelBase, IActivatableViewModel, IReceiver<StartInstallConfiguration>
|
private readonly DTOSerializer _dtos;
|
||||||
|
private readonly InstallationStateManager _stateManager;
|
||||||
|
|
||||||
|
|
||||||
|
public InstallConfigurationViewModel(DTOSerializer dtos, InstallationStateManager stateManager)
|
||||||
{
|
{
|
||||||
private readonly DTOSerializer _dtos;
|
_stateManager = stateManager;
|
||||||
private readonly InstallationStateManager _stateManager;
|
|
||||||
|
|
||||||
[Reactive]
|
_dtos = dtos;
|
||||||
public AbsolutePath ModListPath { get; set; }
|
Activator = new ViewModelActivator();
|
||||||
|
this.WhenActivated(disposables =>
|
||||||
[Reactive]
|
|
||||||
public AbsolutePath Install { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public AbsolutePath Download { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public ModList? ModList { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public IBitmap? ModListImage { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public bool IsReady { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public ReactiveCommand<Unit, Unit> BeginCommand { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public InstallConfigurationViewModel(DTOSerializer dtos, InstallationStateManager stateManager)
|
|
||||||
{
|
{
|
||||||
_stateManager = stateManager;
|
this.ValidationRule(x => x.ModListPath, p => p.FileExists(), "Wabbajack file must exist");
|
||||||
|
this.ValidationRule(x => x.Install, p => p.DirectoryExists(), "Install folder file must exist");
|
||||||
|
this.ValidationRule(x => x.Download, p => p != default, "Download folder must be set");
|
||||||
|
|
||||||
_dtos = dtos;
|
BeginCommand = ReactiveCommand.Create(() => { StartInstall().FireAndForget(); }, this.IsValid());
|
||||||
Activator = new ViewModelActivator();
|
|
||||||
this.WhenActivated(disposables =>
|
|
||||||
{
|
|
||||||
|
|
||||||
this.ValidationRule(x => x.ModListPath, p => p.FileExists(), "Wabbajack file must exist");
|
|
||||||
this.ValidationRule(x => x.Install, p => p.DirectoryExists(), "Install folder file must exist");
|
|
||||||
this.ValidationRule(x => x.Download, p => p != default, "Download folder must be set");
|
|
||||||
|
|
||||||
BeginCommand = ReactiveCommand.Create(() => {StartInstall().FireAndForget();}, this.IsValid());
|
|
||||||
|
|
||||||
|
|
||||||
this.WhenAnyValue(t => t.ModListPath)
|
|
||||||
.Where(t => t != default)
|
|
||||||
.SelectAsync(disposables, async x => await LoadModList(x))
|
|
||||||
.Select(x => x)
|
|
||||||
.ObserveOn(AvaloniaScheduler.Instance)
|
|
||||||
.BindTo(this, t => t.ModList)
|
|
||||||
.DisposeWith(disposables);
|
|
||||||
|
|
||||||
this.WhenAnyValue(t => t.ModListPath)
|
|
||||||
.Where(t => t != default)
|
|
||||||
.SelectAsync(disposables, async x => await LoadModListImage(x))
|
|
||||||
.ObserveOn(AvaloniaScheduler.Instance)
|
|
||||||
.BindTo(this, t => t.ModListImage)
|
|
||||||
.DisposeWith(disposables);
|
|
||||||
|
|
||||||
var settings = this.WhenAnyValue(t => t.ModListPath)
|
|
||||||
.SelectAsync(disposables, async v => await _stateManager.Get(v))
|
|
||||||
.ObserveOn(RxApp.MainThreadScheduler)
|
|
||||||
.Where(s => s != null);
|
|
||||||
|
|
||||||
settings.Select(s => s!.Install)
|
|
||||||
.BindTo(this, vm => vm.Install)
|
|
||||||
.DisposeWith(disposables);
|
|
||||||
|
|
||||||
settings.Select(s => s!.Downloads)
|
|
||||||
.BindTo(this, vm => vm.Download)
|
|
||||||
.DisposeWith(disposables);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
}
|
this.WhenAnyValue(t => t.ModListPath)
|
||||||
|
.Where(t => t != default)
|
||||||
|
.SelectAsync(disposables, async x => await LoadModList(x))
|
||||||
|
.Select(x => x)
|
||||||
|
.ObserveOn(AvaloniaScheduler.Instance)
|
||||||
|
.BindTo(this, t => t.ModList)
|
||||||
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
private async Task StartInstall()
|
this.WhenAnyValue(t => t.ModListPath)
|
||||||
|
.Where(t => t != default)
|
||||||
|
.SelectAsync(disposables, async x => await LoadModListImage(x))
|
||||||
|
.ObserveOn(AvaloniaScheduler.Instance)
|
||||||
|
.BindTo(this, t => t.ModListImage)
|
||||||
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
|
var settings = this.WhenAnyValue(t => t.ModListPath)
|
||||||
|
.SelectAsync(disposables, async v => await _stateManager.Get(v))
|
||||||
|
.ObserveOn(RxApp.MainThreadScheduler)
|
||||||
|
.Where(s => s != null);
|
||||||
|
|
||||||
|
settings.Select(s => s!.Install)
|
||||||
|
.BindTo(this, vm => vm.Install)
|
||||||
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
|
settings.Select(s => s!.Downloads)
|
||||||
|
.BindTo(this, vm => vm.Download)
|
||||||
|
.DisposeWith(disposables);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Reactive] public AbsolutePath ModListPath { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public AbsolutePath Install { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public AbsolutePath Download { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public ModList? ModList { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public IBitmap? ModListImage { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public bool IsReady { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public ReactiveCommand<Unit, Unit> BeginCommand { get; set; }
|
||||||
|
|
||||||
|
public ViewModelActivator Activator { get; }
|
||||||
|
|
||||||
|
public void Receive(StartInstallConfiguration val)
|
||||||
|
{
|
||||||
|
ModListPath = val.ModList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StartInstall()
|
||||||
|
{
|
||||||
|
ModlistMetadata? metadata = null;
|
||||||
|
var metadataPath = ModListPath.WithExtension(Ext.MetaData);
|
||||||
|
if (metadataPath.FileExists())
|
||||||
|
metadata = _dtos.Deserialize<ModlistMetadata>(await metadataPath.ReadAllTextAsync());
|
||||||
|
|
||||||
|
_stateManager.SetLastState(new InstallationConfigurationSetting
|
||||||
{
|
{
|
||||||
ModlistMetadata? metadata = null;
|
ModList = ModListPath,
|
||||||
var metadataPath = ModListPath.WithExtension(Ext.MetaData);
|
Downloads = Download,
|
||||||
if (metadataPath.FileExists())
|
Install = Install,
|
||||||
{
|
Metadata = metadata
|
||||||
metadata = _dtos.Deserialize<ModlistMetadata>(await metadataPath.ReadAllTextAsync());
|
}).FireAndForget();
|
||||||
}
|
|
||||||
|
|
||||||
_stateManager.SetLastState(new InstallationConfigurationSetting
|
MessageBus.Instance.Send(new NavigateTo(typeof(StandardInstallationViewModel)));
|
||||||
{
|
MessageBus.Instance.Send(new StartInstallation(ModListPath, Install, Download, metadata));
|
||||||
ModList = ModListPath,
|
}
|
||||||
Downloads = Download,
|
|
||||||
Install = Install,
|
|
||||||
Metadata = metadata
|
|
||||||
}).FireAndForget();
|
|
||||||
|
|
||||||
MessageBus.Instance.Send(new NavigateTo(typeof(StandardInstallationViewModel)));
|
|
||||||
MessageBus.Instance.Send(new StartInstallation(ModListPath, Install, Download, metadata));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<IBitmap> LoadModListImage(AbsolutePath path)
|
private async Task<IBitmap> LoadModListImage(AbsolutePath path)
|
||||||
{
|
{
|
||||||
return new Bitmap(await ModListUtilities.GetModListImageStream(path));
|
return new Bitmap(await ModListUtilities.GetModListImageStream(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ModList> LoadModList(AbsolutePath modlist)
|
private async Task<ModList> LoadModList(AbsolutePath modlist)
|
||||||
{
|
{
|
||||||
var definition= await StandardInstaller.LoadFromFile(_dtos, modlist);
|
var definition = await StandardInstaller.LoadFromFile(_dtos, modlist);
|
||||||
return definition;
|
return definition;
|
||||||
}
|
|
||||||
|
|
||||||
public ViewModelActivator Activator { get; }
|
|
||||||
|
|
||||||
public void Receive(StartInstallConfiguration val)
|
|
||||||
{
|
|
||||||
ModListPath = val.ModList;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,20 +1,15 @@
|
|||||||
using System;
|
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using CefNet;
|
|
||||||
using JetBrains.Annotations;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Wabbajack.DTOs.Logins;
|
using Wabbajack.DTOs.Logins;
|
||||||
using Wabbajack.Services.OSIntegrated;
|
|
||||||
using Wabbajack.Services.OSIntegrated.TokenProviders;
|
using Wabbajack.Services.OSIntegrated.TokenProviders;
|
||||||
|
|
||||||
namespace Wabbajack.App.ViewModels
|
namespace Wabbajack.App.ViewModels;
|
||||||
|
|
||||||
|
public class LoversLabOAuthLoginViewModel : OAuthLoginViewModel<LoversLabLoginState>
|
||||||
{
|
{
|
||||||
public class LoversLabOAuthLoginViewModel : OAuthLoginViewModel<LoversLabLoginState>
|
public LoversLabOAuthLoginViewModel(ILogger<LoversLabOAuthLoginViewModel> logger, HttpClient client,
|
||||||
|
LoversLabTokenProvider tokenProvider)
|
||||||
|
: base(logger, client, tokenProvider)
|
||||||
{
|
{
|
||||||
public LoversLabOAuthLoginViewModel(ILogger<LoversLabOAuthLoginViewModel> logger, HttpClient client,
|
|
||||||
LoversLabTokenProvider tokenProvider)
|
|
||||||
: base(logger, client, tokenProvider)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -8,7 +8,6 @@ using System.Reactive.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using ReactiveUI.Fody.Helpers;
|
using ReactiveUI.Fody.Helpers;
|
||||||
using ReactiveUI.Validation.Helpers;
|
using ReactiveUI.Validation.Helpers;
|
||||||
@ -21,142 +20,121 @@ using Wabbajack.Common;
|
|||||||
using Wabbajack.Paths.IO;
|
using Wabbajack.Paths.IO;
|
||||||
using Wabbajack.RateLimiter;
|
using Wabbajack.RateLimiter;
|
||||||
|
|
||||||
namespace Wabbajack.App.ViewModels
|
namespace Wabbajack.App.ViewModels;
|
||||||
|
|
||||||
|
public class MainWindowViewModel : ReactiveValidationObject, IActivatableViewModel, IReceiver<NavigateTo>,
|
||||||
|
IReceiver<NavigateBack>
|
||||||
{
|
{
|
||||||
public class MainWindowViewModel : ReactiveValidationObject, IActivatableViewModel, IReceiver<NavigateTo>, IReceiver<NavigateBack>
|
private readonly InstallationStateManager _manager;
|
||||||
|
private readonly IServiceProvider _provider;
|
||||||
|
private readonly Task _resourcePoller;
|
||||||
|
private readonly IResource[] _resources;
|
||||||
|
private readonly IEnumerable<IScreenView> _screens;
|
||||||
|
private StatusReport[] _prevReport;
|
||||||
|
|
||||||
|
public MainWindowViewModel(IEnumerable<IScreenView> screens, IEnumerable<IResource> resources,
|
||||||
|
IServiceProvider provider,
|
||||||
|
InstallationStateManager manager)
|
||||||
{
|
{
|
||||||
private readonly IEnumerable<IScreenView> _screens;
|
_provider = provider;
|
||||||
private readonly IServiceProvider _provider;
|
_screens = screens;
|
||||||
private readonly IResource[] _resources;
|
_resources = resources.ToArray();
|
||||||
private StatusReport[] _prevReport;
|
_manager = manager;
|
||||||
private readonly Task _resourcePoller;
|
|
||||||
private readonly InstallationStateManager _manager;
|
|
||||||
|
|
||||||
[Reactive]
|
_prevReport = NextReport();
|
||||||
public Control CurrentScreen { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
private ImmutableStack<Control> BreadCrumbs { get; set; } = ImmutableStack<Control>.Empty;
|
|
||||||
|
|
||||||
[Reactive]
|
Activator = new ViewModelActivator();
|
||||||
public ReactiveCommand<Unit, Unit> BackButton { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public ReactiveCommand<Unit, Unit> SettingsButton { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
|
||||||
public ReactiveCommand<Unit, Unit> LogViewButton { get; set; }
|
|
||||||
|
|
||||||
[Reactive]
|
_resourcePoller = StartResourcePoller(TimeSpan.FromSeconds(0.25));
|
||||||
public string ResourceStatus { get; set; }
|
|
||||||
|
this.WhenActivated(disposables =>
|
||||||
public MainWindowViewModel(IEnumerable<IScreenView> screens, IEnumerable<IResource> resources, IServiceProvider provider,
|
|
||||||
InstallationStateManager manager)
|
|
||||||
{
|
{
|
||||||
_provider = provider;
|
BackButton = ReactiveCommand.Create(() => { Receive(new NavigateBack()); },
|
||||||
_screens = screens;
|
this.ObservableForProperty(vm => vm.BreadCrumbs)
|
||||||
_resources = resources.ToArray();
|
.Select(bc => bc.Value.Count() > 1))
|
||||||
_manager = manager;
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
_prevReport = NextReport();
|
SettingsButton = ReactiveCommand.Create(() => { Receive(new NavigateTo(typeof(SettingsViewModel))); })
|
||||||
|
.DisposeWith(disposables);
|
||||||
Activator = new ViewModelActivator();
|
|
||||||
|
|
||||||
_resourcePoller = StartResourcePoller(TimeSpan.FromSeconds(0.25));
|
|
||||||
|
|
||||||
this.WhenActivated(disposables =>
|
|
||||||
{
|
|
||||||
BackButton = ReactiveCommand.Create(() =>
|
|
||||||
{
|
|
||||||
Receive(new NavigateBack());
|
|
||||||
},
|
|
||||||
this.ObservableForProperty(vm => vm.BreadCrumbs)
|
|
||||||
.Select(bc => bc.Value.Count() > 1))
|
|
||||||
.DisposeWith(disposables);
|
|
||||||
|
|
||||||
SettingsButton = ReactiveCommand.Create(() =>
|
|
||||||
{
|
|
||||||
Receive(new NavigateTo(typeof(SettingsViewModel)));
|
|
||||||
})
|
|
||||||
.DisposeWith(disposables);
|
|
||||||
|
|
||||||
LogViewButton = ReactiveCommand.Create(() =>
|
|
||||||
{
|
|
||||||
Receive(new NavigateTo(typeof(LogScreenViewModel)));
|
|
||||||
})
|
|
||||||
.DisposeWith(disposables);
|
|
||||||
|
|
||||||
});
|
|
||||||
CurrentScreen = (Control)_screens.First(s => s.ViewModelType == typeof(ModeSelectionViewModel));
|
|
||||||
|
|
||||||
LoadFirstScreen().FireAndForget();
|
LogViewButton = ReactiveCommand.Create(() => { Receive(new NavigateTo(typeof(LogScreenViewModel))); })
|
||||||
|
.DisposeWith(disposables);
|
||||||
|
});
|
||||||
|
CurrentScreen = (Control) _screens.First(s => s.ViewModelType == typeof(ModeSelectionViewModel));
|
||||||
|
|
||||||
|
LoadFirstScreen().FireAndForget();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Reactive] public Control CurrentScreen { get; set; }
|
||||||
|
|
||||||
|
[Reactive] private ImmutableStack<Control> BreadCrumbs { get; set; } = ImmutableStack<Control>.Empty;
|
||||||
|
|
||||||
|
[Reactive] public ReactiveCommand<Unit, Unit> BackButton { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public ReactiveCommand<Unit, Unit> SettingsButton { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public ReactiveCommand<Unit, Unit> LogViewButton { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public string ResourceStatus { get; set; }
|
||||||
|
|
||||||
|
public ViewModelActivator Activator { get; }
|
||||||
|
|
||||||
|
public void Receive(NavigateBack val)
|
||||||
|
{
|
||||||
|
CurrentScreen = BreadCrumbs.Peek();
|
||||||
|
BreadCrumbs = BreadCrumbs.Pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Receive(NavigateTo val)
|
||||||
|
{
|
||||||
|
BreadCrumbs = BreadCrumbs.Push(CurrentScreen);
|
||||||
|
|
||||||
|
if (val.ViewModel.IsAssignableTo(typeof(GuidedWebViewModel)))
|
||||||
|
CurrentScreen = new GuidedWebView {ViewModel = (GuidedWebViewModel) _provider.GetService(val.ViewModel)!};
|
||||||
|
else
|
||||||
|
CurrentScreen = (Control) _screens.First(s => s.ViewModelType == val.ViewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadFirstScreen()
|
||||||
|
{
|
||||||
|
var setting = await _manager.GetLastState();
|
||||||
|
if (setting.Install != default && setting.Install.DirectoryExists())
|
||||||
|
{
|
||||||
|
BreadCrumbs =
|
||||||
|
BreadCrumbs.Push((Control) _screens.First(s => s.ViewModelType == typeof(ModeSelectionViewModel)));
|
||||||
|
|
||||||
|
MessageBus.Instance.Send(new ConfigureLauncher(setting.Install));
|
||||||
|
Receive(new NavigateTo(typeof(LauncherViewModel)));
|
||||||
}
|
}
|
||||||
|
else
|
||||||
private async Task LoadFirstScreen()
|
|
||||||
{
|
{
|
||||||
var setting = await _manager.GetLastState();
|
Receive(new NavigateTo(typeof(ModeSelectionViewModel)));
|
||||||
if (setting.Install != default && setting.Install.DirectoryExists())
|
|
||||||
{
|
|
||||||
BreadCrumbs =
|
|
||||||
BreadCrumbs.Push((Control)_screens.First(s => s.ViewModelType == typeof(ModeSelectionViewModel)));
|
|
||||||
|
|
||||||
MessageBus.Instance.Send(new ConfigureLauncher(setting.Install));
|
|
||||||
Receive(new NavigateTo(typeof(LauncherViewModel)));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Receive(new NavigateTo(typeof(ModeSelectionViewModel)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private StatusReport[] NextReport()
|
|
||||||
{
|
|
||||||
return _resources.Select(r => r.StatusReport).ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task StartResourcePoller(TimeSpan span)
|
|
||||||
{
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
var report = NextReport();
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
foreach (var (prev, next, limiter) in _prevReport.Zip(report, _resources))
|
|
||||||
{
|
|
||||||
var throughput = next.Transferred - prev.Transferred;
|
|
||||||
if (throughput != 0)
|
|
||||||
{
|
|
||||||
sb.Append(
|
|
||||||
$"{limiter.Name}: [{next.Running}/{next.Pending + next.Running}] {throughput.ToFileSizeString()}/sec ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ResourceStatus = sb.ToString();
|
|
||||||
_prevReport = report;
|
|
||||||
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(0.5));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ViewModelActivator Activator { get; }
|
|
||||||
public void Receive(NavigateTo val)
|
|
||||||
{
|
|
||||||
BreadCrumbs = BreadCrumbs.Push(CurrentScreen);
|
|
||||||
|
|
||||||
if (val.ViewModel.IsAssignableTo(typeof(GuidedWebViewModel)))
|
|
||||||
{
|
|
||||||
CurrentScreen = new GuidedWebView() { ViewModel = (GuidedWebViewModel)_provider.GetService(val.ViewModel)! };
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
CurrentScreen = (Control)_screens.First(s => s.ViewModelType == val.ViewModel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Receive(NavigateBack val)
|
|
||||||
{
|
|
||||||
CurrentScreen = BreadCrumbs.Peek();
|
|
||||||
BreadCrumbs = BreadCrumbs.Pop();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private StatusReport[] NextReport()
|
||||||
|
{
|
||||||
|
return _resources.Select(r => r.StatusReport).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StartResourcePoller(TimeSpan span)
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var report = NextReport();
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
foreach (var (prev, next, limiter) in _prevReport.Zip(report, _resources))
|
||||||
|
{
|
||||||
|
var throughput = next.Transferred - prev.Transferred;
|
||||||
|
if (throughput != 0)
|
||||||
|
sb.Append(
|
||||||
|
$"{limiter.Name}: [{next.Running}/{next.Pending + next.Running}] {throughput.ToFileSizeString()}/sec ");
|
||||||
|
}
|
||||||
|
|
||||||
|
ResourceStatus = sb.ToString();
|
||||||
|
_prevReport = report;
|
||||||
|
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(0.5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +1,11 @@
|
|||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using ReactiveUI.Fody.Helpers;
|
|
||||||
|
|
||||||
namespace Wabbajack.App.ViewModels
|
namespace Wabbajack.App.ViewModels;
|
||||||
|
|
||||||
|
public class ModeSelectionViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
public class ModeSelectionViewModel : ViewModelBase
|
public ModeSelectionViewModel()
|
||||||
{
|
{
|
||||||
public ModeSelectionViewModel()
|
Activator = new ViewModelActivator();
|
||||||
{
|
|
||||||
Activator = new ViewModelActivator();
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,97 +9,92 @@ using Wabbajack.App.Messages;
|
|||||||
using Wabbajack.DTOs.Logins;
|
using Wabbajack.DTOs.Logins;
|
||||||
using Wabbajack.Services.OSIntegrated.TokenProviders;
|
using Wabbajack.Services.OSIntegrated.TokenProviders;
|
||||||
|
|
||||||
namespace Wabbajack.App.ViewModels
|
namespace Wabbajack.App.ViewModels;
|
||||||
|
|
||||||
|
public class NexusLoginViewModel : GuidedWebViewModel
|
||||||
{
|
{
|
||||||
public class NexusLoginViewModel : GuidedWebViewModel
|
private readonly NexusApiTokenProvider _tokenProvider;
|
||||||
|
|
||||||
|
public NexusLoginViewModel(ILogger<NexusLoginViewModel> logger, NexusApiTokenProvider tokenProvider) : base(logger)
|
||||||
{
|
{
|
||||||
private readonly NexusApiTokenProvider _tokenProvider;
|
_tokenProvider = tokenProvider;
|
||||||
|
}
|
||||||
|
|
||||||
public NexusLoginViewModel(ILogger<NexusLoginViewModel> logger, NexusApiTokenProvider tokenProvider) : base(logger)
|
public override async Task Run(CancellationToken token)
|
||||||
{
|
{
|
||||||
_tokenProvider = tokenProvider;
|
token.ThrowIfCancellationRequested();
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task Run(CancellationToken token)
|
Instructions = "Please log into the Nexus";
|
||||||
|
|
||||||
|
await Browser.WaitForReady();
|
||||||
|
|
||||||
|
await Browser.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 Browser.Cookies("nexusmods.com", token);
|
||||||
|
if (cookies.Any(c => c.Name == "member_id"))
|
||||||
|
break;
|
||||||
|
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
|
await Task.Delay(500, token);
|
||||||
Instructions = "Please log into the Nexus";
|
|
||||||
|
|
||||||
await Browser.WaitForReady();
|
|
||||||
|
|
||||||
await Browser.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 Browser.Cookies("nexusmods.com", token);
|
|
||||||
if (cookies.Any(c => c.Name == "member_id"))
|
|
||||||
break;
|
|
||||||
|
|
||||||
token.ThrowIfCancellationRequested();
|
|
||||||
await Task.Delay(500, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
Instructions = "Getting API Key...";
|
|
||||||
|
|
||||||
await Browser.NavigateTo(new Uri("https://www.nexusmods.com/users/myaccount?tab=api"));
|
|
||||||
|
|
||||||
var key = "";
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
key = (await Browser.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 Browser.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);
|
|
||||||
|
|
||||||
MessageBus.Instance.Send(new NavigateBack());
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Instructions = "Success, saving information...";
|
|
||||||
await _tokenProvider.SetToken(new NexusApiState
|
|
||||||
{
|
|
||||||
Cookies = cookies,
|
|
||||||
ApiKey = key
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Instructions = "Getting API Key...";
|
||||||
|
|
||||||
|
await Browser.NavigateTo(new Uri("https://www.nexusmods.com/users/myaccount?tab=api"));
|
||||||
|
|
||||||
|
var key = "";
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
key = (await Browser.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 Browser.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);
|
||||||
|
|
||||||
|
MessageBus.Instance.Send(new NavigateBack());
|
||||||
|
}
|
||||||
|
|
||||||
|
Instructions = "Success, saving information...";
|
||||||
|
await _tokenProvider.SetToken(new NexusApiState
|
||||||
|
{
|
||||||
|
Cookies = cookies,
|
||||||
|
ApiKey = key
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -12,118 +12,109 @@ using Wabbajack.App.Extensions;
|
|||||||
using Wabbajack.DTOs.Logins;
|
using Wabbajack.DTOs.Logins;
|
||||||
using Wabbajack.Services.OSIntegrated;
|
using Wabbajack.Services.OSIntegrated;
|
||||||
|
|
||||||
namespace Wabbajack.App.ViewModels
|
namespace Wabbajack.App.ViewModels;
|
||||||
{
|
|
||||||
public abstract class OAuthLoginViewModel<TLoginType> : GuidedWebViewModel
|
public abstract class OAuthLoginViewModel<TLoginType> : GuidedWebViewModel
|
||||||
where TLoginType : OAuth2LoginState, new()
|
where TLoginType : OAuth2LoginState, new()
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly EncryptedJsonTokenProvider<TLoginType> _tokenProvider;
|
||||||
|
|
||||||
|
public OAuthLoginViewModel(ILogger logger, HttpClient httpClient,
|
||||||
|
EncryptedJsonTokenProvider<TLoginType> tokenProvider) : base(logger)
|
||||||
{
|
{
|
||||||
private readonly HttpClient _httpClient;
|
_logger = logger;
|
||||||
private readonly EncryptedJsonTokenProvider<TLoginType> _tokenProvider;
|
_httpClient = httpClient;
|
||||||
|
_tokenProvider = tokenProvider;
|
||||||
public OAuthLoginViewModel(ILogger logger, HttpClient httpClient, EncryptedJsonTokenProvider<TLoginType> tokenProvider) : base(logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_httpClient = httpClient;
|
|
||||||
_tokenProvider = tokenProvider;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private class AsyncSchemeHandler : CefSchemeHandlerFactory
|
|
||||||
{
|
|
||||||
private TaskCompletionSource<Uri> _tcs = new();
|
|
||||||
public Task<Uri> Task => _tcs.Task;
|
|
||||||
|
|
||||||
public AsyncSchemeHandler()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override CefResourceHandler Create(CefBrowser browser, CefFrame frame, string schemeName,
|
|
||||||
CefRequest request)
|
|
||||||
{
|
|
||||||
return new Handler(_tcs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class Handler : CefResourceHandler
|
|
||||||
{
|
|
||||||
private readonly TaskCompletionSource<Uri> _tcs;
|
|
||||||
|
|
||||||
public Handler(TaskCompletionSource<Uri> tcs)
|
|
||||||
{
|
|
||||||
_tcs = tcs;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool ProcessRequest(CefRequest request, CefCallback callback)
|
|
||||||
{
|
|
||||||
_tcs.TrySetResult(new Uri(request.Url));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task Run(CancellationToken token)
|
|
||||||
{
|
|
||||||
var tlogin = new TLoginType();
|
|
||||||
|
|
||||||
await Browser.WaitForReady();
|
|
||||||
|
|
||||||
var handler = new AsyncSchemeHandler();
|
|
||||||
Browser.RequestContext.RegisterSchemeHandlerFactory("wabbajack", "", handler);
|
|
||||||
|
|
||||||
Instructions = $"Please log in and allow Wabbajack to access your {tlogin.SiteName} account";
|
|
||||||
|
|
||||||
var scopes = string.Join(" ", tlogin.Scopes);
|
|
||||||
var state = Guid.NewGuid().ToString();
|
|
||||||
|
|
||||||
await Browser.NavigateTo(new Uri(tlogin.AuthorizationEndpoint +
|
|
||||||
$"?response_type=code&client_id={tlogin.ClientID}&state={state}&scope={scopes}"));
|
|
||||||
|
|
||||||
var uri = await handler.Task.WaitAsync(token);
|
|
||||||
|
|
||||||
var cookies = await Browser.Cookies(tlogin.AuthorizationEndpoint.Host, token);
|
|
||||||
|
|
||||||
var parsed = HttpUtility.ParseQueryString(uri.Query);
|
|
||||||
if (parsed.Get("state") != state)
|
|
||||||
{
|
|
||||||
_logger.LogCritical("Bad OAuth state, this shouldn't happen");
|
|
||||||
throw new Exception("Bad OAuth State");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsed.Get("code") == null)
|
|
||||||
{
|
|
||||||
_logger.LogCritical("Bad code result from OAuth");
|
|
||||||
throw new Exception("Bad code result from OAuth");
|
|
||||||
}
|
|
||||||
|
|
||||||
var authCode = parsed.Get("code");
|
|
||||||
|
|
||||||
var formData = new KeyValuePair<string?, string?>[]
|
|
||||||
{
|
|
||||||
new("grant_type", "authorization_code"),
|
|
||||||
new("code", authCode),
|
|
||||||
new("client_id", tlogin.ClientID)
|
|
||||||
};
|
|
||||||
|
|
||||||
var msg = new HttpRequestMessage();
|
|
||||||
msg.Method = HttpMethod.Post;
|
|
||||||
msg.RequestUri = tlogin.TokenEndpoint;
|
|
||||||
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(";", cookies.Select(c => $"{c.Name}={c.Value}")));
|
|
||||||
msg.Content = new FormUrlEncodedContent(formData.ToList());
|
|
||||||
|
|
||||||
using var response = await _httpClient.SendAsync(msg, token);
|
|
||||||
var data = await response.Content.ReadFromJsonAsync<OAuthResultState>(cancellationToken: token);
|
|
||||||
|
|
||||||
await _tokenProvider.SetToken(new TLoginType
|
|
||||||
{
|
|
||||||
Cookies = cookies,
|
|
||||||
ResultState = data!
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override async Task Run(CancellationToken token)
|
||||||
|
{
|
||||||
|
var tlogin = new TLoginType();
|
||||||
|
|
||||||
|
await Browser.WaitForReady();
|
||||||
|
|
||||||
|
var handler = new AsyncSchemeHandler();
|
||||||
|
Browser.RequestContext.RegisterSchemeHandlerFactory("wabbajack", "", handler);
|
||||||
|
|
||||||
|
Instructions = $"Please log in and allow Wabbajack to access your {tlogin.SiteName} account";
|
||||||
|
|
||||||
|
var scopes = string.Join(" ", tlogin.Scopes);
|
||||||
|
var state = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
await Browser.NavigateTo(new Uri(tlogin.AuthorizationEndpoint +
|
||||||
|
$"?response_type=code&client_id={tlogin.ClientID}&state={state}&scope={scopes}"));
|
||||||
|
|
||||||
|
var uri = await handler.Task.WaitAsync(token);
|
||||||
|
|
||||||
|
var cookies = await Browser.Cookies(tlogin.AuthorizationEndpoint.Host, token);
|
||||||
|
|
||||||
|
var parsed = HttpUtility.ParseQueryString(uri.Query);
|
||||||
|
if (parsed.Get("state") != state)
|
||||||
|
{
|
||||||
|
_logger.LogCritical("Bad OAuth state, this shouldn't happen");
|
||||||
|
throw new Exception("Bad OAuth State");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.Get("code") == null)
|
||||||
|
{
|
||||||
|
_logger.LogCritical("Bad code result from OAuth");
|
||||||
|
throw new Exception("Bad code result from OAuth");
|
||||||
|
}
|
||||||
|
|
||||||
|
var authCode = parsed.Get("code");
|
||||||
|
|
||||||
|
var formData = new KeyValuePair<string?, string?>[]
|
||||||
|
{
|
||||||
|
new("grant_type", "authorization_code"),
|
||||||
|
new("code", authCode),
|
||||||
|
new("client_id", tlogin.ClientID)
|
||||||
|
};
|
||||||
|
|
||||||
|
var msg = new HttpRequestMessage();
|
||||||
|
msg.Method = HttpMethod.Post;
|
||||||
|
msg.RequestUri = tlogin.TokenEndpoint;
|
||||||
|
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(";", cookies.Select(c => $"{c.Name}={c.Value}")));
|
||||||
|
msg.Content = new FormUrlEncodedContent(formData.ToList());
|
||||||
|
|
||||||
|
using var response = await _httpClient.SendAsync(msg, token);
|
||||||
|
var data = await response.Content.ReadFromJsonAsync<OAuthResultState>(cancellationToken: token);
|
||||||
|
|
||||||
|
await _tokenProvider.SetToken(new TLoginType
|
||||||
|
{
|
||||||
|
Cookies = cookies,
|
||||||
|
ResultState = data!
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private class AsyncSchemeHandler : CefSchemeHandlerFactory
|
||||||
|
{
|
||||||
|
private readonly TaskCompletionSource<Uri> _tcs = new();
|
||||||
|
|
||||||
|
public Task<Uri> Task => _tcs.Task;
|
||||||
|
|
||||||
|
protected override CefResourceHandler Create(CefBrowser browser, CefFrame frame, string schemeName,
|
||||||
|
CefRequest request)
|
||||||
|
{
|
||||||
|
return new Handler(_tcs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Handler : CefResourceHandler
|
||||||
|
{
|
||||||
|
private readonly TaskCompletionSource<Uri> _tcs;
|
||||||
|
|
||||||
|
public Handler(TaskCompletionSource<Uri> tcs)
|
||||||
|
{
|
||||||
|
_tcs = tcs;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool ProcessRequest(CefRequest request, CefCallback callback)
|
||||||
|
{
|
||||||
|
_tcs.TrySetResult(new Uri(request.Url));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -8,37 +8,30 @@ using ReactiveUI;
|
|||||||
using ReactiveUI.Fody.Helpers;
|
using ReactiveUI.Fody.Helpers;
|
||||||
using Wabbajack.DTOs.DownloadStates;
|
using Wabbajack.DTOs.DownloadStates;
|
||||||
|
|
||||||
namespace Wabbajack.App.ViewModels.SubViewModels
|
namespace Wabbajack.App.ViewModels.SubViewModels;
|
||||||
|
|
||||||
|
public class SlideViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
public class SlideViewModel : ViewModelBase
|
public SlideViewModel()
|
||||||
{
|
{
|
||||||
[Reactive]
|
Activator = new ViewModelActivator();
|
||||||
public IMetaState MetaState { get; set; }
|
Image = null;
|
||||||
|
}
|
||||||
[Reactive]
|
|
||||||
public IImage? Image { get; set; }
|
|
||||||
|
|
||||||
public bool Loading { get; set; } = false;
|
[Reactive] public IMetaState MetaState { get; set; }
|
||||||
|
|
||||||
public SlideViewModel()
|
[Reactive] public IImage? Image { get; set; }
|
||||||
{
|
|
||||||
Activator = new ViewModelActivator();
|
|
||||||
Image = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PreCache(HttpClient client)
|
public bool Loading { get; set; }
|
||||||
{
|
|
||||||
Loading = true;
|
|
||||||
var url = await client.GetByteArrayAsync(MetaState.ImageURL);
|
|
||||||
var img = new Bitmap(new MemoryStream(url));
|
|
||||||
|
|
||||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
|
||||||
{
|
|
||||||
Image = img;
|
|
||||||
});
|
|
||||||
|
|
||||||
Loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public async Task PreCache(HttpClient client)
|
||||||
|
{
|
||||||
|
Loading = true;
|
||||||
|
var url = await client.GetByteArrayAsync(MetaState.ImageURL);
|
||||||
|
var img = new Bitmap(new MemoryStream(url));
|
||||||
|
|
||||||
|
await Dispatcher.UIThread.InvokeAsync(() => { Image = img; });
|
||||||
|
|
||||||
|
Loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user