mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
UI fixes and optimizations
This commit is contained in:
parent
a4de95889a
commit
5d1bc5ff3b
@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Threading;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -24,6 +26,7 @@ public class App : Application
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => Thread.CurrentThread.Name = "UIThread");
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,11 @@
|
||||
using System;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Wabbajack.App.Extensions;
|
||||
|
||||
@ -21,4 +25,34 @@ public static class IObservableExtensions
|
||||
|
||||
return returnObs;
|
||||
}
|
||||
|
||||
public static IDisposable SimpleOneWayBind<TView, TViewModel, TProp, TOut>(
|
||||
this TView view,
|
||||
TViewModel? viewModel,
|
||||
Expression<Func<TViewModel, TProp?>> vmProperty,
|
||||
Expression<Func<TView, TOut?>> viewProperty)
|
||||
where TView : class
|
||||
{
|
||||
var d = viewModel.WhenAny(vmProperty, change => change.Value)
|
||||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
.BindTo(view, viewProperty);
|
||||
|
||||
return Disposable.Create(() => d.Dispose());
|
||||
}
|
||||
|
||||
public static IDisposable SimpleOneWayBind<TView, TViewModel, TProp, TOut>(
|
||||
this TView view,
|
||||
TViewModel? viewModel,
|
||||
Expression<Func<TViewModel, TProp?>> vmProperty,
|
||||
Expression<Func<TView, TOut?>> viewProperty,
|
||||
Func<TProp?, TOut> selector)
|
||||
where TView : class
|
||||
{
|
||||
var d = viewModel.WhenAnyValue(vmProperty)
|
||||
.Select(change => selector(change))
|
||||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
.BindTo(view, viewProperty);
|
||||
|
||||
return Disposable.Create(() => d.Dispose());
|
||||
}
|
||||
}
|
@ -48,7 +48,7 @@ public class LauncherViewModel : ViewModelBase
|
||||
.DisposeWith(disposables);
|
||||
|
||||
this.WhenAnyValue(v => v.Setting)
|
||||
.Where(v => v != default && v!.Image != default)
|
||||
.Where(v => v != default && v!.Image != default && v!.Image.FileExists())
|
||||
.Select(v => new Bitmap(v!.Image.ToString()))
|
||||
.BindTo(this, vm => vm.Image)
|
||||
.DisposeWith(disposables);
|
||||
|
@ -7,8 +7,8 @@
|
||||
x:Class="Wabbajack.App.Views.StandardInstallationView">
|
||||
<Grid RowDefinitions="40, 5, 5, *, 40">
|
||||
<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 Grid.Row="2" x:Name="StepProgress" Maximum="10000" Value="30" />
|
||||
<ProgressBar Grid.Row="1" x:Name="StepsProgress" Maximum="1000" Value="0" />
|
||||
<ProgressBar Grid.Row="2" x:Name="StepProgress" Maximum="10000" Value="0" />
|
||||
<Viewbox Grid.Row="3" HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Stretch="Uniform">
|
||||
|
@ -1,5 +1,9 @@
|
||||
using System;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using Avalonia.Threading;
|
||||
using ReactiveUI;
|
||||
using Wabbajack.App.Extensions;
|
||||
using Wabbajack.App.ViewModels;
|
||||
|
||||
namespace Wabbajack.App.Views;
|
||||
@ -27,14 +31,15 @@ public partial class StandardInstallationView : ScreenBase<StandardInstallationV
|
||||
this.BindCommand(ViewModel, vm => vm.PlayCommand, view => view.PlaySlides)
|
||||
.DisposeWith(disposables);
|
||||
|
||||
this.OneWayBind(ViewModel, vm => vm.StatusText, view => view.StatusText.Text)
|
||||
this.SimpleOneWayBind(ViewModel, vm => vm.StatusText, view => view.StatusText.Text)
|
||||
.DisposeWith(disposables);
|
||||
|
||||
this.OneWayBind(ViewModel, vm => vm.StepsProgress, view => view.StepsProgress.Value, p => p.Value * 1000)
|
||||
this.SimpleOneWayBind(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)
|
||||
this.SimpleOneWayBind(ViewModel, vm => vm.StepProgress, p => p.StepProgress.Value, p => p.Value * 10000)
|
||||
.DisposeWith(disposables);
|
||||
|
||||
});
|
||||
}
|
||||
}
|
@ -44,6 +44,7 @@ public class StandardInstallationViewModel : ViewModelBase
|
||||
private IServiceScope _scope;
|
||||
private SlideViewModel[] _slides = Array.Empty<SlideViewModel>();
|
||||
private Timer _slideTimer;
|
||||
private Timer _updateTimer;
|
||||
|
||||
public StandardInstallationViewModel(ILogger<StandardInstallationViewModel> logger, IServiceProvider provider,
|
||||
GameLocator locator, DTOSerializer dtos,
|
||||
@ -63,6 +64,9 @@ public class StandardInstallationViewModel : ViewModelBase
|
||||
|
||||
this.WhenActivated(disposables =>
|
||||
{
|
||||
_updateTimer = new Timer(UpdateStatus, null, TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(250));
|
||||
_updateTimer.DisposeWith(disposables);
|
||||
|
||||
_slideTimer = new Timer(_ =>
|
||||
{
|
||||
if (IsPlaying) NextSlide(1);
|
||||
@ -102,12 +106,23 @@ public class StandardInstallationViewModel : ViewModelBase
|
||||
[Reactive] public string StatusText { get; set; } = "";
|
||||
[Reactive] public Percent StepsProgress { get; set; } = Percent.Zero;
|
||||
[Reactive] public Percent StepProgress { get; set; } = Percent.Zero;
|
||||
|
||||
// Not Reactive, so we don't end up spamming the UI threads with events
|
||||
public StatusUpdate _latestStatus { get; set; } = new("", Percent.Zero, Percent.Zero);
|
||||
|
||||
public void Receive(StartInstallation msg)
|
||||
{
|
||||
Install(msg).FireAndForget();
|
||||
}
|
||||
|
||||
private void UpdateStatus(object? state)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => {
|
||||
StepsProgress = _latestStatus.StepsProgress;
|
||||
StepProgress = _latestStatus.StepProgress;
|
||||
StatusText = _latestStatus.StatusText;
|
||||
});
|
||||
}
|
||||
|
||||
private void NextSlide(int direction)
|
||||
{
|
||||
@ -175,21 +190,12 @@ public class StandardInstallationViewModel : ViewModelBase
|
||||
|
||||
_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);
|
||||
};
|
||||
_installer.OnStatusUpdate = update => _latestStatus = update;
|
||||
|
||||
_logger.LogInformation("Installer created, starting the installation process");
|
||||
try
|
||||
{
|
||||
var result = await _installer.Begin(CancellationToken.None);
|
||||
var result = await Task.Run(async () => await _installer.Begin(CancellationToken.None));
|
||||
if (!result) throw new Exception("Installation failed");
|
||||
|
||||
if (result) await SaveConfigAndContinue(_config);
|
||||
@ -200,6 +206,7 @@ public class StandardInstallationViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task SaveConfigAndContinue(InstallerConfiguration config)
|
||||
{
|
||||
var path = config.Install.Combine("modlist-image.png");
|
||||
|
@ -67,7 +67,8 @@ public class FileExtractor
|
||||
Predicate<RelativePath> shouldExtract,
|
||||
Func<RelativePath, IExtractedFile, ValueTask<T>> mapfn,
|
||||
CancellationToken token,
|
||||
HashSet<RelativePath>? onlyFiles = null)
|
||||
HashSet<RelativePath>? onlyFiles = null,
|
||||
Action<Percent>? progressFunction = null)
|
||||
{
|
||||
if (sFn is NativeFileStreamFactory) _logger.LogInformation("Extracting {file}", sFn.Name);
|
||||
await using var archive = await sFn.GetStream();
|
||||
@ -92,7 +93,7 @@ public class FileExtractor
|
||||
{
|
||||
await using var tempFolder = _manager.CreateFolder();
|
||||
results = await GatheringExtractWith7Zip(sFn, shouldExtract,
|
||||
mapfn, onlyFiles, token);
|
||||
mapfn, onlyFiles, token, progressFunction);
|
||||
}
|
||||
|
||||
break;
|
||||
@ -179,7 +180,8 @@ public class FileExtractor
|
||||
Predicate<RelativePath> shouldExtract,
|
||||
Func<RelativePath, IExtractedFile, ValueTask<T>> mapfn,
|
||||
IReadOnlyCollection<RelativePath>? onlyFiles,
|
||||
CancellationToken token)
|
||||
CancellationToken token,
|
||||
Action<Percent>? progressFunction = null)
|
||||
{
|
||||
TemporaryPath? tmpFile = null;
|
||||
await using var dest = _manager.CreateFolder();
|
||||
@ -262,6 +264,9 @@ public class FileExtractor
|
||||
var newPosition = percentInt == 0 ? 0 : totalSize / percentInt;
|
||||
var throughput = newPosition - oldPosition;
|
||||
job.ReportNoWait((int) throughput);
|
||||
|
||||
progressFunction?.Invoke(Percent.FactoryPutInRange(lastPercent, 100));
|
||||
|
||||
lastPercent = percentInt;
|
||||
}, token);
|
||||
|
||||
@ -306,7 +311,7 @@ public class FileExtractor
|
||||
}
|
||||
|
||||
public async Task ExtractAll(AbsolutePath src, AbsolutePath dest, CancellationToken token,
|
||||
Predicate<RelativePath>? filterFn = null)
|
||||
Predicate<RelativePath>? filterFn = null, Action<Percent>? updateProgress = null)
|
||||
{
|
||||
filterFn ??= _ => true;
|
||||
await GatheringExtract(new NativeFileStreamFactory(src), filterFn, async (path, factory) =>
|
||||
|
@ -51,11 +51,11 @@ public abstract class AInstaller<T>
|
||||
private long _currentStepProgress;
|
||||
|
||||
|
||||
private long _maxStepProgress;
|
||||
protected long MaxStepProgress { get; set; }
|
||||
private string _statusText;
|
||||
private readonly Stopwatch _updateStopWatch = new();
|
||||
|
||||
public Func<StatusUpdate, Task>? OnStatusUpdate;
|
||||
public Action<StatusUpdate>? OnStatusUpdate;
|
||||
|
||||
|
||||
public AInstaller(ILogger<T> logger, InstallerConfiguration config, IGameLocator gameLocator,
|
||||
@ -90,38 +90,30 @@ public abstract class AInstaller<T>
|
||||
public async Task NextStep(string statusText, long maxStepProgress)
|
||||
{
|
||||
_updateStopWatch.Restart();
|
||||
_maxStepProgress = maxStepProgress;
|
||||
MaxStepProgress = maxStepProgress;
|
||||
_currentStep += 1;
|
||||
_statusText = statusText;
|
||||
_logger.LogInformation("Next Step: {Step}", statusText);
|
||||
|
||||
if (OnStatusUpdate != null)
|
||||
await OnStatusUpdate!(new StatusUpdate($"[{_currentStep}/{MaxSteps}] " + statusText,
|
||||
Percent.FactoryPutInRange(_currentStep, MaxSteps), Percent.Zero));
|
||||
OnStatusUpdate?.Invoke(new StatusUpdate($"[{_currentStep}/{MaxSteps}] " + statusText,
|
||||
Percent.FactoryPutInRange(_currentStep, MaxSteps), Percent.Zero));
|
||||
}
|
||||
|
||||
public async ValueTask UpdateProgress(long stepProgress)
|
||||
public void UpdateProgress(long stepProgress)
|
||||
{
|
||||
Interlocked.Add(ref _currentStepProgress, stepProgress);
|
||||
|
||||
if (_updateStopWatch.ElapsedMilliseconds < _limitMS) return;
|
||||
lock (_updateStopWatch)
|
||||
{
|
||||
if (_updateStopWatch.ElapsedMilliseconds < _limitMS) return;
|
||||
_updateStopWatch.Restart();
|
||||
}
|
||||
|
||||
if (OnStatusUpdate != null)
|
||||
await OnStatusUpdate!(new StatusUpdate(_statusText, Percent.FactoryPutInRange(_currentStep, MaxSteps),
|
||||
Percent.FactoryPutInRange(_currentStepProgress, _maxStepProgress)));
|
||||
OnStatusUpdate?.Invoke(new StatusUpdate($"[{_currentStep}/{MaxSteps}] " + _statusText, Percent.FactoryPutInRange(_currentStep, MaxSteps),
|
||||
Percent.FactoryPutInRange(_currentStepProgress, MaxStepProgress)));
|
||||
}
|
||||
|
||||
public abstract Task<bool> Begin(CancellationToken token);
|
||||
|
||||
public async Task ExtractModlist(CancellationToken token)
|
||||
{
|
||||
await NextStep("Extracting Modlist", 100);
|
||||
ExtractedModlistFolder = _manager.CreateFolder();
|
||||
await _extractor.ExtractAll(_configuration.ModlistArchive, ExtractedModlistFolder, token);
|
||||
await _extractor.ExtractAll(_configuration.ModlistArchive, ExtractedModlistFolder, token, updateProgress: p => UpdateProgress((long)(p.Value * 100)));
|
||||
}
|
||||
|
||||
public async Task<byte[]> LoadBytesFromPath(RelativePath path)
|
||||
@ -165,13 +157,15 @@ public abstract class AInstaller<T>
|
||||
/// </summary>
|
||||
protected async Task PrimeVFS()
|
||||
{
|
||||
await NextStep("Priming VFS", 0);
|
||||
_vfs.AddKnown(_configuration.ModList.Directives.OfType<FromArchive>().Select(d => d.ArchiveHashPath),
|
||||
HashedArchives);
|
||||
await _vfs.BackfillMissing();
|
||||
}
|
||||
|
||||
public void BuildFolderStructure()
|
||||
{
|
||||
public async Task BuildFolderStructure()
|
||||
{
|
||||
await NextStep("Building Folder Structure", 0);
|
||||
_logger.LogInformation("Building Folder Structure");
|
||||
ModList.Directives
|
||||
.Where(d => d.To.Depth > 1)
|
||||
@ -196,7 +190,7 @@ public abstract class AInstaller<T>
|
||||
foreach (var directive in grouped[vf])
|
||||
{
|
||||
var file = directive.Directive;
|
||||
await UpdateProgress(file.Size);
|
||||
UpdateProgress(file.Size);
|
||||
|
||||
switch (file)
|
||||
{
|
||||
@ -305,7 +299,7 @@ public abstract class AInstaller<T>
|
||||
}
|
||||
|
||||
await DownloadArchive(archive, download, token, outputPath);
|
||||
await UpdateProgress(1);
|
||||
UpdateProgress(1);
|
||||
});
|
||||
}
|
||||
|
||||
@ -345,23 +339,30 @@ public abstract class AInstaller<T>
|
||||
|
||||
public async Task HashArchives(CancellationToken token)
|
||||
{
|
||||
await NextStep("Hashing Archives", 0);
|
||||
_logger.LogInformation("Looking for files to hash");
|
||||
|
||||
var allFiles = _configuration.Downloads.EnumerateFiles()
|
||||
.Concat(_gameLocator.GameLocation(_configuration.Game).EnumerateFiles())
|
||||
.ToList();
|
||||
|
||||
|
||||
var hashDict = allFiles.GroupBy(f => f.Size()).ToDictionary(g => g.Key);
|
||||
|
||||
var toHash = ModList.Archives.Where(a => hashDict.ContainsKey(a.Size))
|
||||
.SelectMany(a => hashDict[a.Size]).ToList();
|
||||
|
||||
MaxStepProgress = toHash.Count;
|
||||
|
||||
_logger.LogInformation("Found {count} total files, {hashedCount} matching filesize", allFiles.Count,
|
||||
toHash.Count);
|
||||
|
||||
var hashResults = await
|
||||
toHash
|
||||
.PMapAll(async e => (await _fileHashCache.FileHashCachedAsync(e, token), e))
|
||||
.PMapAll(async e =>
|
||||
{
|
||||
UpdateProgress(1);
|
||||
return (await _fileHashCache.FileHashCachedAsync(e, token), e);
|
||||
})
|
||||
.ToList();
|
||||
|
||||
HashedArchives = hashResults
|
||||
@ -393,7 +394,7 @@ public abstract class AInstaller<T>
|
||||
await existingFiles
|
||||
.PDoAll(async f =>
|
||||
{
|
||||
await UpdateProgress(1);
|
||||
UpdateProgress(1);
|
||||
var relativeTo = f.RelativeTo(_configuration.Install);
|
||||
if (indexed.ContainsKey(relativeTo) || f.InFolder(_configuration.Downloads))
|
||||
return;
|
||||
@ -409,11 +410,11 @@ public abstract class AInstaller<T>
|
||||
|
||||
_logger.LogInformation("Cleaning empty folders");
|
||||
await NextStep("Optimizing Modlist: Cleaning empty folders", indexed.Keys.Count);
|
||||
var expectedFolders = (await indexed.Keys
|
||||
var expectedFolders = (indexed.Keys
|
||||
.Select(f => f.RelativeTo(_configuration.Install))
|
||||
// We ignore the last part of the path, so we need a dummy file name
|
||||
.Append(_configuration.Downloads.Combine("_"))
|
||||
.OnEach(async _ => await UpdateProgress(1))
|
||||
.OnEach(_ => UpdateProgress(1))
|
||||
.Where(f => f.InFolder(_configuration.Install))
|
||||
.SelectMany(path =>
|
||||
{
|
||||
@ -446,7 +447,7 @@ public abstract class AInstaller<T>
|
||||
await NextStep("Optimizing Modlist: Removing redundant directives", indexed.Count);
|
||||
await indexed.Values.PMapAll<Directive, Directive?>(async d =>
|
||||
{
|
||||
await UpdateProgress(1);
|
||||
UpdateProgress(1);
|
||||
// Bit backwards, but we want to return null for
|
||||
// all files we *want* installed. We return the files
|
||||
// to remove from the install list.
|
||||
@ -462,6 +463,7 @@ public abstract class AInstaller<T>
|
||||
|
||||
_logger.LogInformation("Optimized {optimized} directives to {indexed} required", ModList.Directives.Length,
|
||||
indexed.Count);
|
||||
await NextStep("Finalizing modlist optimization", 0);
|
||||
var requiredArchives = indexed.Values.OfType<FromArchive>()
|
||||
.GroupBy(d => d.ArchiveHashPath.Hash)
|
||||
.Select(d => d.Key)
|
||||
|
@ -38,13 +38,14 @@ public class StandardInstaller : AInstaller<StandardInstaller>
|
||||
base(logger, config, gameLocator, extractor, jsonSerializer, vfs, fileHashCache, downloadDispatcher,
|
||||
parallelOptions, wjClient)
|
||||
{
|
||||
MaxSteps = 7;
|
||||
MaxSteps = 13;
|
||||
}
|
||||
|
||||
public override async Task<bool> Begin(CancellationToken token)
|
||||
{
|
||||
if (token.IsCancellationRequested) return false;
|
||||
await _wjClient.SendMetric(MetricNames.BeginInstall, ModList.Name);
|
||||
await NextStep("Configuring Installer", 0);
|
||||
_logger.LogInformation("Configuring Processor");
|
||||
|
||||
if (_configuration.GameFolder == default)
|
||||
@ -84,8 +85,7 @@ public class StandardInstaller : AInstaller<StandardInstaller>
|
||||
_configuration.Downloads.CreateDirectory();
|
||||
|
||||
await OptimizeModlist(token);
|
||||
|
||||
|
||||
|
||||
await HashArchives(token);
|
||||
|
||||
await DownloadArchives(token);
|
||||
@ -264,7 +264,7 @@ public class StandardInstaller : AInstaller<StandardInstaller>
|
||||
.OfType<InlineFile>()
|
||||
.PDoAll(async directive =>
|
||||
{
|
||||
await UpdateProgress(1);
|
||||
UpdateProgress(1);
|
||||
var outPath = _configuration.Install.Combine(directive.To);
|
||||
outPath.Delete();
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
<PackageReference Include="Avalonia.Desktop" Version="0.10.10" />
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.10" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.10" />
|
||||
<PackageReference Include="ReactiveUI.Fody" Version="16.2.6" />
|
||||
<PackageReference Include="ReactiveUI.Fody" Version="16.3.5" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Wabbajack.Paths.IO\Wabbajack.Paths.IO.csproj" />
|
||||
|
Loading…
Reference in New Issue
Block a user