Code cleanup and fix test running

This commit is contained in:
Timothy Baldridge 2021-10-23 10:51:17 -06:00
parent 2b5662a15b
commit f99f4a7538
577 changed files with 26516 additions and 27096 deletions

View File

@ -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

View File

@ -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();

View File

@ -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);
} }

View File

@ -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);
} }
} }

View File

@ -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>

View File

@ -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>

View File

@ -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());
}
} }
} }

View File

@ -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>

View File

@ -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

View File

@ -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; }
}
} }

View File

@ -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>

View File

@ -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)
};
}
} }
} }

View File

@ -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();
} }
} }

View File

@ -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>

View File

@ -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);
}
} }
} }

View File

@ -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!;
} }

View File

@ -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>

View File

@ -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);
});
}
} }
} }

View File

@ -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; }
} }

View File

@ -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>

View File

@ -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);
}); });
} }
} }

View File

@ -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; }
} }

View File

@ -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>

View File

@ -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);
});
}
} }
} }

View File

@ -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>

View File

@ -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;

View File

@ -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>

View File

@ -16,5 +16,4 @@ public partial class LogViewItem : ReactiveUserControl<LoggerProvider.ILogMessag
.DisposeWith(disposables); .DisposeWith(disposables);
}); });
} }
} }

View File

@ -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; }
} }

View File

@ -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>

View File

@ -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);
}); });
} }
} }

View File

@ -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; }
} }

View File

@ -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>

View File

@ -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);
}); });
} }
} }

View File

@ -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();
}
} }

View File

@ -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>

View File

@ -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);
}); });
}
} }
} }

View File

@ -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; }
} }

View File

@ -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;
}
} }
} }
} }

View File

@ -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;
} }
} }

View File

@ -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
{
}
} }

View File

@ -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;
} }
} }

View File

@ -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;
}
} }
} }

View File

@ -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>

View File

@ -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);
}
} }

View File

@ -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; }
}
} }

View File

@ -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;
}); });
}
} }
} }

View File

@ -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)
{
}
} }

View File

@ -4,5 +4,4 @@ namespace Wabbajack.App.Messages;
public record Error(string Prefix, Exception Exception) public record Error(string Prefix, Exception Exception)
{ {
} }

View File

@ -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);
}
} }

View File

@ -1,10 +1,5 @@
namespace Wabbajack.App.Messages namespace Wabbajack.App.Messages;
public class NavigateBack
{ {
public class NavigateBack
{
public NavigateBack()
{
}
}
} }

View File

@ -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)
{
}
} }

View File

@ -4,5 +4,4 @@ namespace Wabbajack.App.Messages;
public record StartCompilation(CompilerSettings Settings) public record StartCompilation(CompilerSettings Settings)
{ {
} }

View File

@ -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)
{
}
} }

View File

@ -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)
{
}
} }

View File

@ -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);
}
} }

View File

@ -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);
});
}
} }

View File

@ -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)
{ {

View File

@ -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)
{
}
}

View File

@ -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">

View File

@ -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);
}); });
}
} }
} }

View File

@ -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; }
}
} }

View File

@ -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>

View File

@ -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);
}); });
} }
} }

View File

@ -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();
} }

View File

@ -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">

View File

@ -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());
});
}
} }
} }

View File

@ -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);
} }

View File

@ -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>

View File

@ -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)));
} }
} }

View File

@ -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>

View File

@ -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);
});
} }
} }

View File

@ -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");
} }
} }

View File

@ -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>

View File

@ -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()
{ {

View File

@ -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();
} }
} }

View File

@ -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>

View File

@ -15,5 +15,4 @@ public partial class PlaySelectView : ScreenBase<PlaySelectViewModel>
.DisposeWith(disposables); .DisposeWith(disposables);
}); });
} }
} }

View File

@ -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();
} }
} }

View File

@ -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>

View File

@ -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);
});
}
} }
} }

View File

@ -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);
} }
} }

View File

@ -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>

View File

@ -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);
});
} }
} }

View File

@ -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)));
}
} }

View File

@ -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);
} }
} }

View File

@ -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}"};
}
} }
} }

View File

@ -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);
}
} }
} }

View File

@ -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

View File

@ -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());
}
} }
} }

View File

@ -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());
}
} }
} }

View File

@ -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;
}
} }
} }

View File

@ -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);
} }

View File

@ -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;
}
} }
} }

View File

@ -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)
{
}
} }
} }

View File

@ -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));
}
}
}

View File

@ -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();
}
} }
} }

View File

@ -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
});
} }
} }

View File

@ -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;
}
}
} }

View File

@ -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