Merge pull request #1639 from wabbajack-tools/launch-page

Launch page
This commit is contained in:
Timothy Baldridge 2021-09-30 16:54:18 -06:00 committed by GitHub
commit 3180678a87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 315 additions and 19 deletions

View File

@ -4,6 +4,7 @@ using System.Linq;
using System.Net.Http;
using System.Reactive;
using System.Reactive.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Media.Imaging;
@ -17,6 +18,7 @@ using Wabbajack.App.ViewModels;
using Wabbajack.Common;
using Wabbajack.Downloaders;
using Wabbajack.DTOs;
using Wabbajack.DTOs.JsonConverters;
using Wabbajack.Installer;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
@ -43,6 +45,7 @@ namespace Wabbajack.App.Controls
private readonly DownloadDispatcher _dispatcher;
private readonly ILogger _logger;
private readonly IResource<DownloadDispatcher> _downloadLimiter;
private readonly DTOSerializer _dtos;
public string Title => _metadata.ImageContainsTitle ? "" : _metadata.Title;
public string MachineURL => _metadata.Links.MachineURL;
@ -74,7 +77,7 @@ namespace Wabbajack.App.Controls
public BrowseItemViewModel(ModlistMetadata metadata, ModListSummary summary, HttpClient client, IResource<HttpClient> limiter,
FileHashCache hashCache, Configuration configuration, DownloadDispatcher dispatcher, IResource<DownloadDispatcher> downloadLimiter, GameLocator gameLocator,
ILogger logger)
DTOSerializer dtos, ILogger logger)
{
Activator = new ViewModelActivator();
_metadata = metadata;
@ -86,6 +89,8 @@ namespace Wabbajack.App.Controls
_dispatcher = dispatcher;
_downloadLimiter = downloadLimiter;
_logger = logger;
_dtos = dtos;
var haveGame = gameLocator.IsInstalled(_metadata.Game);
Tags = metadata.tags
.Select(t => new TagViewModel(t, "ModList"))
@ -150,6 +155,9 @@ namespace Wabbajack.App.Controls
_hashCache.FileHashWriteCache(ModListLocation, hash);
var metadataPath = ModListLocation.WithExtension(Ext.MetaData);
await metadataPath.WriteAllTextAsync(_dtos.Serialize(_metadata));
await UpdateState();
}

View File

@ -0,0 +1,9 @@
using Wabbajack.Paths;
namespace Wabbajack.App.Messages
{
public record ConfigureLauncher(AbsolutePath InstallFolder)
{
}
}

View File

@ -3,7 +3,7 @@ using Wabbajack.Paths;
namespace Wabbajack.App.Messages
{
public record StartInstallation(AbsolutePath ModListPath, AbsolutePath Install, AbsolutePath Download)
public record StartInstallation(AbsolutePath ModListPath, AbsolutePath Install, AbsolutePath Download, ModlistMetadata? Metadata)
{
}
}

View File

@ -73,5 +73,10 @@ namespace Wabbajack.App.Models
{
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

@ -24,6 +24,7 @@ using Wabbajack.Networking.WabbajackClientApi;
using DynamicData.Binding;
using Microsoft.Extensions.DependencyInjection;
using Wabbajack.Downloaders;
using Wabbajack.DTOs.JsonConverters;
using Wabbajack.Installer;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
@ -52,6 +53,7 @@ namespace Wabbajack.App.Screens
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]
@ -67,7 +69,7 @@ namespace Wabbajack.App.Screens
[Reactive] public bool ShowNSFW { get; set; } = false;
public BrowseViewModel(ILogger<BrowseViewModel> logger, Client wjClient, HttpClient httpClient, IResource<HttpClient> limiter, FileHashCache hashCache,
IResource<DownloadDispatcher> dispatcherLimiter, DownloadDispatcher dispatcher, GameLocator gameLocator, Configuration configuration)
IResource<DownloadDispatcher> dispatcherLimiter, DownloadDispatcher dispatcher, GameLocator gameLocator, DTOSerializer dtos, Configuration configuration)
{
Activator = new ViewModelActivator();
_wjClient = wjClient;
@ -79,6 +81,7 @@ namespace Wabbajack.App.Screens
_dispatcher = dispatcher;
_dispatcherLimiter = dispatcherLimiter;
_gameLocator = gameLocator;
_dtos = dtos;
IObservable<Func<BrowseItemViewModel, bool>> searchTextPredicates = this.ObservableForProperty(vm => vm.SearchText)
@ -198,7 +201,7 @@ namespace Wabbajack.App.Screens
summary = new ModListSummary();
}
return new BrowseItemViewModel(m, summary, _httpClient, _limiter, _hashCache, _configuration, _dispatcher, _dispatcherLimiter, _gameLocator, _logger);
return new BrowseItemViewModel(m, summary, _httpClient, _limiter, _hashCache, _configuration, _dispatcher, _dispatcherLimiter, _gameLocator, _dtos, _logger);
});
_modLists.Edit(lsts =>

View File

@ -0,0 +1,37 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:Wabbajack.App.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Wabbajack.App.Screens.LauncherView">
<Grid RowDefinitions="*, Auto">
<Viewbox Grid.Row="0"
Name="Banner"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stretch="UniformToFill">
<Image x:Name="ModListImage" Margin="0,0,0,0" Source="../Assets/Wabba_Mouth.png" />
</Viewbox>
<Grid Grid.Row="0" RowDefinitions="40, 40" HorizontalAlignment="Left" VerticalAlignment="Bottom">
<TextBlock x:Name="ModListName"></TextBlock>
</Grid>
<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>
<TextBox Grid.Column="1" Grid.Row="0" IsEnabled="False" Height="20" x:Name="ModList"></TextBox>
<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>
<Grid Grid.Column="1" Grid.Row="3" Grid.ColumnDefinitions="*, *, *" HorizontalAlignment="Center">
<Button Grid.Column="0" x:Name="WebsiteButton">Website</Button>
<Button Grid.Column="1" x:Name="ReadmeButton">Readme</Button>
<Button Grid.Column="2" x:Name="LocalFilesButton">Local Files</Button>
</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>
</Grid>
</Grid>
</UserControl>

View File

@ -0,0 +1,33 @@
using System.Reactive.Disposables;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ReactiveUI;
using Wabbajack.App.Views;
namespace Wabbajack.App.Screens
{
public partial class LauncherView : ScreenBase<LauncherViewModel>
{
public LauncherView()
{
InitializeComponent();
this.WhenActivated(disposables =>
{
this.OneWayBind(ViewModel, vm => vm.Image, view => view.ModListImage.Source)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.Title, view => view.ModList.Text)
.DisposeWith(disposables);
this.OneWayBind(ViewModel, vm => vm.InstallFolder, view => view.InstallPath.Text,
v => v.ToString())
.DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.PlayButton, view => view.PlayGame.Button)
.DisposeWith(disposables);
});
}
}
}

View File

@ -0,0 +1,102 @@
using System.Diagnostics;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Media.Imaging;
using GameFinder.StoreHandlers.Origin.DTO;
using Microsoft.CodeAnalysis;
using Microsoft.Extensions.Logging;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.App.Extensions;
using Wabbajack.App.Messages;
using Wabbajack.App.Models;
using Wabbajack.App.ViewModels;
using Wabbajack.Common;
using Wabbajack.DTOs;
using Wabbajack.DTOs.SavedSettings;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
namespace Wabbajack.App.Screens
{
public class LauncherViewModel : ViewModelBase, IActivatableViewModel, IReceiver<ConfigureLauncher>
{
[Reactive]
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();
PlayButton = ReactiveCommand.Create(() =>
{
StartGame().FireAndForget();
});
_logger = logger;
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)
.Where(v => v != default)
.Select(v => new Bitmap((v!.Image).ToString()))
.BindTo(this, vm => vm.Image)
.DisposeWith(disposables);
this.WhenAnyValue(v => v.Setting)
.Where(v => v is { Metadata: { } })
.Select(v => $"{v!.Metadata!.Title} v{v!.Metadata.Version}")
.BindTo(this, vm => vm.Title)
.DisposeWith(disposables);
});
}
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");
}
}
public void Receive(ConfigureLauncher val)
{
InstallFolder = val.InstallFolder;
}
}
}

View File

@ -12,7 +12,7 @@
<Viewbox Grid.Row="3" HorizontalAlignment="Center"
VerticalAlignment="Center"
Stretch="Uniform">
<Image x:Name="SlideImage" Source="C:\tmp\modlist-image.png"></Image>
<Image x:Name="SlideImage"></Image>
</Viewbox>
<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>

View File

@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reactive;
@ -13,12 +14,17 @@ using Microsoft.Extensions.Logging;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.App.Messages;
using Wabbajack.App.Models;
using Wabbajack.App.Screens;
using Wabbajack.App.Utilities;
using Wabbajack.App.ViewModels.SubViewModels;
using Wabbajack.Common;
using Wabbajack.DTOs;
using Wabbajack.DTOs.DownloadStates;
using Wabbajack.DTOs.JsonConverters;
using Wabbajack.DTOs.SavedSettings;
using Wabbajack.Installer;
using Wabbajack.Paths.IO;
using Wabbajack.RateLimiter;
namespace Wabbajack.App.ViewModels
@ -36,6 +42,7 @@ namespace Wabbajack.App.ViewModels
private readonly HttpClient _httpClient;
private Timer _slideTimer;
private int _currentSlideIndex;
private readonly InstallationStateManager _installStateManager;
[Reactive]
public SlideViewModel Slide { get; set; }
@ -58,13 +65,15 @@ namespace Wabbajack.App.ViewModels
[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)
public StandardInstallationViewModel(ILogger<StandardInstallationViewModel> logger, IServiceProvider provider, GameLocator locator, DTOSerializer dtos,
HttpClient httpClient, InstallationStateManager manager)
{
_provider = provider;
_locator = locator;
_logger = logger;
_dtos = dtos;
_httpClient = httpClient;
_installStateManager = manager;
Activator = new ViewModelActivator();
this.WhenActivated(disposables => {
@ -142,6 +151,7 @@ namespace Wabbajack.App.ViewModels
_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);
@ -180,7 +190,34 @@ namespace Wabbajack.App.ViewModels
};
_logger.LogInformation("Installer created, starting the installation process");
await _installer.Begin(CancellationToken.None);
var result = await _installer.Begin(CancellationToken.None);
if (result)
{
await SaveConfigAndContinue(_config);
}
}
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

@ -46,6 +46,8 @@ namespace Wabbajack.App
services.AddSingleton<IScreenView, StandardInstallationView>();
services.AddSingleton<IScreenView, SettingsView>();
services.AddSingleton<IScreenView, BrowseView>();
services.AddSingleton<IScreenView, LauncherView>();
services.AddSingleton<InstallationStateManager>();
services.AddSingleton<HttpClient>();
@ -56,6 +58,7 @@ namespace Wabbajack.App
services.AddAllSingleton<IReceiverMarker, NexusLoginViewModel>();
services.AddAllSingleton<IReceiverMarker, LoversLabOAuthLoginViewModel>();
services.AddAllSingleton<IReceiverMarker, VectorPlexusOAuthLoginViewModel>();
services.AddAllSingleton<IReceiverMarker, LauncherViewModel>();
// Services
services.AddAllSingleton<IDownloader, IDownloader<Manual>, ManualDownloader>();

View File

@ -0,0 +1,21 @@
using System.IO;
using System.IO.Compression;
using System.Threading.Tasks;
using Wabbajack.Common;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
namespace Wabbajack.App.Utilities
{
public class ModListUtilities
{
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);
var entry = ar.GetEntry("modlist-image.png");
await using var stream = entry!.Open();
return new MemoryStream(await stream.ReadAllAsync());
}
}
}

View File

@ -12,6 +12,7 @@ using ReactiveUI.Validation.Extensions;
using Wabbajack.App.Extensions;
using Wabbajack.App.Messages;
using Wabbajack.App.Models;
using Wabbajack.App.Utilities;
using Wabbajack.Common;
using Wabbajack.DTOs;
using Wabbajack.DTOs.JsonConverters;
@ -63,7 +64,7 @@ namespace Wabbajack.App.ViewModels
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, this.IsValid());
BeginCommand = ReactiveCommand.Create(() => {StartInstall().FireAndForget();}, this.IsValid());
this.WhenAnyValue(t => t.ModListPath)
@ -97,26 +98,30 @@ namespace Wabbajack.App.ViewModels
}
private void StartInstall()
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
{
ModList = ModListPath,
Downloads = Download,
Install = Install
Install = Install,
Metadata = metadata
}).FireAndForget();
MessageBus.Instance.Send(new NavigateTo(typeof(StandardInstallationViewModel)));
MessageBus.Instance.Send(new StartInstallation(ModListPath, Install, Download));
MessageBus.Instance.Send(new StartInstallation(ModListPath, Install, Download, metadata));
}
private async Task<IBitmap> LoadModListImage(AbsolutePath path)
{
await using var fs = path.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
using var ar = new ZipArchive(fs, ZipArchiveMode.Read);
var entry = ar.GetEntry("modlist-image.png");
await using var stream = entry!.Open();
return new Bitmap(new MemoryStream(await stream.ReadAllAsync()));
return new Bitmap(await ModListUtilities.GetModListImageStream(path));
}
private async Task<ModList> LoadModList(AbsolutePath modlist)

View File

@ -14,9 +14,11 @@ using ReactiveUI.Fody.Helpers;
using ReactiveUI.Validation.Helpers;
using Wabbajack.App.Interfaces;
using Wabbajack.App.Messages;
using Wabbajack.App.Models;
using Wabbajack.App.Screens;
using Wabbajack.App.Views;
using Wabbajack.Common;
using Wabbajack.Paths.IO;
using Wabbajack.RateLimiter;
namespace Wabbajack.App.ViewModels
@ -28,6 +30,7 @@ namespace Wabbajack.App.ViewModels
private readonly IResource[] _resources;
private StatusReport[] _prevReport;
private readonly Task _resourcePoller;
private readonly InstallationStateManager _manager;
[Reactive]
public Control CurrentScreen { get; set; }
@ -44,11 +47,13 @@ namespace Wabbajack.App.ViewModels
[Reactive]
public string ResourceStatus { get; set; }
public MainWindowViewModel(IEnumerable<IScreenView> screens, IEnumerable<IResource> resources, IServiceProvider provider)
public MainWindowViewModel(IEnumerable<IScreenView> screens, IEnumerable<IResource> resources, IServiceProvider provider,
InstallationStateManager manager)
{
_provider = provider;
_screens = screens;
_resources = resources.ToArray();
_manager = manager;
_prevReport = NextReport();
@ -73,9 +78,26 @@ namespace Wabbajack.App.ViewModels
.DisposeWith(disposables);
});
Receive(new NavigateTo(typeof(ModeSelectionViewModel)));
LoadFirstScreen().FireAndForget();
}
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
{
Receive(new NavigateTo(typeof(ModeSelectionViewModel)));
}
}
private StatusReport[] NextReport()

View File

@ -41,6 +41,10 @@
<DependentUpon>SettingsView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Screens\StandardInstallationView.axaml.cs">
<DependentUpon>StandardInstallationView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
<Target Name="AferBuild" AfterTargets="Build">

View File

@ -19,5 +19,6 @@ namespace Wabbajack.Common
public static Extension Dds = new(".dds");
public static Extension Json = new(".json");
public static Extension Md = new(".md");
public static Extension MetaData = new(".metadata");
}
}

View File

@ -16,5 +16,9 @@ namespace Wabbajack.DTOs.SavedSettings
public AbsolutePath ModList { get; set; }
public AbsolutePath Install { get; set; }
public AbsolutePath Downloads { get; set; }
public ModlistMetadata? Metadata { get; set; }
public AbsolutePath Image { get; set; }
}
}

View File

@ -12,5 +12,7 @@ namespace Wabbajack.Installer
public SystemParameters? SystemParameters { get; set; }
public Game Game { get; set; }
public AbsolutePath GameFolder { get; set; }
public ModlistMetadata? Metadata { get; set; }
}
}