mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
Merge branch 'main' into upgrade-deps
This commit is contained in:
commit
8e792e6092
@ -1,8 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Threading;
|
||||||
using Avalonia;
|
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 Avalonia.Threading;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@ -24,6 +26,7 @@ public class App : Application
|
|||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
|
Dispatcher.UIThread.Post(() => Thread.CurrentThread.Name = "UIThread");
|
||||||
AvaloniaXamlLoader.Load(this);
|
AvaloniaXamlLoader.Load(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +37,31 @@
|
|||||||
|
|
||||||
<Style Selector="Border.Settings">
|
<Style Selector="Border.Settings">
|
||||||
<Setter Property="BorderThickness" Value="2"></Setter>
|
<Setter Property="BorderThickness" Value="2"></Setter>
|
||||||
|
<Setter Property="BorderBrush" Value="DarkGray"></Setter>
|
||||||
|
<Setter Property="CornerRadius" Value="4"></Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Border.Settings Grid">
|
||||||
|
<Setter Property="Margin" Value="4"></Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Grid.LogView ItemsControl">
|
||||||
|
<Setter Property="Margin" Value="4"></Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Grid.LogView > TextBlock.Title">
|
||||||
|
<Setter Property="FontWeight" Value="Bold"></Setter>
|
||||||
|
<Setter Property="FontSize" Value="18"></Setter>
|
||||||
|
<Setter Property="Margin" Value="4"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Grid.LogView > Border">
|
||||||
|
<Setter Property="Margin" Value="4"/>
|
||||||
|
<Setter Property="BorderThickness" Value="2"></Setter>
|
||||||
|
<Setter Property="BorderBrush" Value="DarkGray"></Setter>
|
||||||
|
<Setter Property="CornerRadius" Value="4"></Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</Styles>
|
</Styles>
|
@ -30,7 +30,7 @@ public partial class FileSelectionBox : ReactiveUserControl<FileSelectionBoxView
|
|||||||
|
|
||||||
this.WhenActivated(disposables =>
|
this.WhenActivated(disposables =>
|
||||||
{
|
{
|
||||||
this.Bind(ViewModel, vm => vm.Path, view => view.SelectedPath)
|
this.OneWayBind(ViewModel, vm => vm.Path, view => view.SelectedPath)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
this.WhenAnyValue(view => view.SelectFolder)
|
this.WhenAnyValue(view => view.SelectFolder)
|
||||||
.BindTo(ViewModel, vm => vm.SelectFolder)
|
.BindTo(ViewModel, vm => vm.SelectFolder)
|
||||||
@ -41,7 +41,7 @@ public partial class FileSelectionBox : ReactiveUserControl<FileSelectionBoxView
|
|||||||
exts.Split("|", StringSplitOptions.RemoveEmptyEntries).Select(s => new Extension(s)).ToArray())
|
exts.Split("|", StringSplitOptions.RemoveEmptyEntries).Select(s => new Extension(s)).ToArray())
|
||||||
.BindTo(ViewModel, vm => vm.Extensions)
|
.BindTo(ViewModel, vm => vm.Extensions)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
this.Bind(ViewModel, vm => vm.Path,
|
this.OneWayBind(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,
|
||||||
@ -67,4 +67,9 @@ public partial class FileSelectionBox : ReactiveUserControl<FileSelectionBoxView
|
|||||||
get => GetValue(SelectFolderProperty);
|
get => GetValue(SelectFolderProperty);
|
||||||
set => SetValue(SelectFolderProperty, value);
|
set => SetValue(SelectFolderProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Load(AbsolutePath path)
|
||||||
|
{
|
||||||
|
ViewModel.Path = path;
|
||||||
|
}
|
||||||
}
|
}
|
@ -6,9 +6,10 @@
|
|||||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
xmlns:avalonia="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.LogView">
|
x:Class="Wabbajack.App.Controls.LogView">
|
||||||
<Grid RowDefinitions="Auto, *, Auto">
|
<Grid RowDefinitions="Auto, *, Auto" Classes="LogView">
|
||||||
<TextBlock Grid.Row="0">Current Log Contents</TextBlock>
|
<TextBlock Grid.Row="0" Classes="Title">Current Log Contents</TextBlock>
|
||||||
<ScrollViewer Grid.Row="1" ScrollChanged="ScrollViewer_OnScrollChanged" x:Name="ScrollViewer"
|
<Border Grid.Row="1">
|
||||||
|
<ScrollViewer ScrollChanged="ScrollViewer_OnScrollChanged" x:Name="ScrollViewer"
|
||||||
HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible">
|
HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible">
|
||||||
<ItemsControl x:Name="Messages">
|
<ItemsControl x:Name="Messages">
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsControl.ItemsPanel>
|
||||||
@ -23,6 +24,7 @@
|
|||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
<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 Kind="ContentCopy" />
|
||||||
@ -32,4 +34,5 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
</UserControl>
|
</UserControl>
|
@ -1,24 +1,43 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Linq.Expressions;
|
||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
|
using System.Reactive.Linq;
|
||||||
using System.Reactive.Subjects;
|
using System.Reactive.Subjects;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
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,
|
public static IDisposable SimpleOneWayBind<TView, TViewModel, TProp, TOut>(
|
||||||
CompositeDisposable disposable,
|
this TView view,
|
||||||
Func<TIn, ValueTask<TOut>> func)
|
TViewModel? viewModel,
|
||||||
|
Expression<Func<TViewModel, TProp?>> vmProperty,
|
||||||
|
Expression<Func<TView, TOut?>> viewProperty)
|
||||||
|
where TView : class
|
||||||
{
|
{
|
||||||
Subject<TOut> returnObs = new();
|
var d = viewModel.WhenAny(vmProperty, change => change.Value)
|
||||||
|
.ObserveOn(RxApp.MainThreadScheduler)
|
||||||
|
.BindTo(view, viewProperty);
|
||||||
|
|
||||||
input.Subscribe(x => Task.Run(async () =>
|
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 result = await func(x);
|
var d = viewModel.WhenAnyValue(vmProperty)
|
||||||
returnObs.OnNext(result);
|
.Select(change => selector(change))
|
||||||
})).DisposeWith(disposable);
|
.ObserveOn(RxApp.MainThreadScheduler)
|
||||||
|
.BindTo(view, viewProperty);
|
||||||
|
|
||||||
return returnObs;
|
return Disposable.Create(() => d.Dispose());
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
using System.Reactive.Linq;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
namespace Wabbajack.App.Extensions;
|
namespace Wabbajack.App.Extensions;
|
||||||
|
|
||||||
public static class ReactiveUIExtensions
|
public static class ReactiveUIExtensions
|
||||||
{
|
{
|
||||||
|
public static IObservable<T> OnUIThread<T>(this IObservable<T> src)
|
||||||
|
{
|
||||||
|
return src.ObserveOn(AvaloniaScheduler.Instance);
|
||||||
|
}
|
||||||
}
|
}
|
@ -5,4 +5,5 @@ namespace Wabbajack.App.Interfaces;
|
|||||||
public interface IScreenView
|
public interface IScreenView
|
||||||
{
|
{
|
||||||
public Type ViewModelType { get; }
|
public Type ViewModelType { get; }
|
||||||
|
public string HumanName { get; }
|
||||||
}
|
}
|
@ -48,11 +48,13 @@ public class SettingsManager
|
|||||||
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)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Loading settings {Key}", key);
|
_logger.LogWarning(ex, "Loading settings {Key}", key);
|
||||||
|
@ -6,7 +6,7 @@ namespace Wabbajack.App.Screens;
|
|||||||
|
|
||||||
public partial class BrowseView : ScreenBase<BrowseViewModel>
|
public partial class BrowseView : ScreenBase<BrowseViewModel>
|
||||||
{
|
{
|
||||||
public BrowseView()
|
public BrowseView() : base("Web Browser")
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
this.WhenActivated(disposables =>
|
this.WhenActivated(disposables =>
|
||||||
|
@ -6,7 +6,7 @@ namespace Wabbajack.App.Screens;
|
|||||||
|
|
||||||
public partial class CompilationView : ScreenBase<CompilationViewModel>
|
public partial class CompilationView : ScreenBase<CompilationViewModel>
|
||||||
{
|
{
|
||||||
public CompilationView()
|
public CompilationView() : base("Compiling")
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ namespace Wabbajack.App.Screens;
|
|||||||
|
|
||||||
public partial class CompilerConfigurationView : ScreenBase<CompilerConfigurationViewModel>
|
public partial class CompilerConfigurationView : ScreenBase<CompilerConfigurationViewModel>
|
||||||
{
|
{
|
||||||
public CompilerConfigurationView()
|
public CompilerConfigurationView() : base("Compiler Configuration")
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
AddAlwaysEnabled.Command = ReactiveCommand.Create(() => AddAlwaysEnabled_Command().FireAndForget());
|
AddAlwaysEnabled.Command = ReactiveCommand.Create(() => AddAlwaysEnabled_Command().FireAndForget());
|
||||||
|
@ -6,7 +6,7 @@ namespace Wabbajack.App.Screens;
|
|||||||
|
|
||||||
public partial class ErrorPageView : ScreenBase<ErrorPageViewModel>
|
public partial class ErrorPageView : ScreenBase<ErrorPageViewModel>
|
||||||
{
|
{
|
||||||
public ErrorPageView()
|
public ErrorPageView() : base("Error")
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
this.WhenActivated(disposables =>
|
this.WhenActivated(disposables =>
|
||||||
|
@ -6,7 +6,7 @@ namespace Wabbajack.App.Screens;
|
|||||||
|
|
||||||
public partial class LauncherView : ScreenBase<LauncherViewModel>
|
public partial class LauncherView : ScreenBase<LauncherViewModel>
|
||||||
{
|
{
|
||||||
public LauncherView()
|
public LauncherView() : base("Launch Modlist")
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
this.WhenActivated(disposables =>
|
this.WhenActivated(disposables =>
|
||||||
|
@ -35,20 +35,20 @@ public class LauncherViewModel : ViewModelBase
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
MessageBus.Current.Listen<ConfigureLauncher>()
|
MessageBus.Current.Listen<ConfigureLauncher>()
|
||||||
.Subscribe(v => Receive(v))
|
.Subscribe(Receive)
|
||||||
.DisposeWith(VMDisposables);
|
.DisposeWith(VMDisposables);
|
||||||
|
|
||||||
this.WhenActivated(disposables =>
|
this.WhenActivated(disposables =>
|
||||||
{
|
{
|
||||||
this.WhenAnyValue(v => v.InstallFolder)
|
this.WhenAnyValue(v => v.InstallFolder)
|
||||||
.SelectAsync(disposables, async folder => await manager.GetByInstallFolder(folder))
|
.SelectMany(async folder => await manager.GetByInstallFolder(folder))
|
||||||
.ObserveOn(RxApp.MainThreadScheduler)
|
.OnUIThread()
|
||||||
.Where(v => v != null)
|
.Where(v => v != null)
|
||||||
.BindTo(this, vm => vm.Setting)
|
.BindTo(this, vm => vm.Setting)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.WhenAnyValue(v => v.Setting)
|
this.WhenAnyValue(v => v.Setting)
|
||||||
.Where(v => v != default)
|
.Where(v => v != default && v!.Image != default && v!.Image.FileExists())
|
||||||
.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);
|
||||||
|
@ -4,7 +4,7 @@ namespace Wabbajack.App.Screens;
|
|||||||
|
|
||||||
public partial class LogScreenView : ScreenBase<LogScreenViewModel>
|
public partial class LogScreenView : ScreenBase<LogScreenViewModel>
|
||||||
{
|
{
|
||||||
public LogScreenView()
|
public LogScreenView() : base("Application Log")
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ namespace Wabbajack.App.Screens;
|
|||||||
|
|
||||||
public partial class PlaySelectView : ScreenBase<PlaySelectViewModel>
|
public partial class PlaySelectView : ScreenBase<PlaySelectViewModel>
|
||||||
{
|
{
|
||||||
public PlaySelectView()
|
public PlaySelectView() : base("Modlist Selection")
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
this.WhenActivated(disposables =>
|
this.WhenActivated(disposables =>
|
||||||
|
@ -6,7 +6,7 @@ namespace Wabbajack.App.Screens;
|
|||||||
|
|
||||||
public partial class SettingsView : ScreenBase<SettingsViewModel>
|
public partial class SettingsView : ScreenBase<SettingsViewModel>
|
||||||
{
|
{
|
||||||
public SettingsView()
|
public SettingsView() : base("Settings")
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
this.WhenActivated(disposables =>
|
this.WhenActivated(disposables =>
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
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 Grid.Row="1" x:Name="StepsProgress" Maximum="1000" Value="0" />
|
||||||
<ProgressBar Grid.Row="2" x:Name="StepProgress" Maximum="10000" Value="30" />
|
<ProgressBar Grid.Row="2" x:Name="StepProgress" Maximum="10000" Value="0" />
|
||||||
<Viewbox Grid.Row="3" HorizontalAlignment="Center"
|
<Viewbox Grid.Row="3" HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Stretch="Uniform">
|
Stretch="Uniform">
|
||||||
|
@ -1,18 +1,22 @@
|
|||||||
|
using System;
|
||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
|
using System.Reactive.Linq;
|
||||||
|
using Avalonia.Threading;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
using Wabbajack.App.Extensions;
|
||||||
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() : base("Installing")
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
this.WhenActivated(disposables =>
|
this.WhenActivated(disposables =>
|
||||||
{
|
{
|
||||||
this.Bind(ViewModel, vm => vm.Slide.Image, view => view.SlideImage.Source)
|
this.OneWayBind(ViewModel, vm => vm.Slide.Image, view => view.SlideImage.Source)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.BindCommand(ViewModel, vm => vm.NextCommand, view => view.NextSlide)
|
this.BindCommand(ViewModel, vm => vm.NextCommand, view => view.NextSlide)
|
||||||
@ -33,8 +37,9 @@ public partial class StandardInstallationView : ScreenBase<StandardInstallationV
|
|||||||
this.OneWayBind(ViewModel, vm => vm.StepsProgress, view => view.StepsProgress.Value, p => p.Value * 1000)
|
this.OneWayBind(ViewModel, vm => vm.StepsProgress, view => view.StepsProgress.Value, p => p.Value * 1000)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.OneWayBind(ViewModel, vm => vm.StepProgress, view => view.StepProgress.Value, p => p.Value * 10000)
|
this.OneWayBind(ViewModel, vm => vm.StepProgress, p => p.StepProgress.Value, p => p.Value * 10000)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -20,6 +20,7 @@ using Wabbajack.App.Utilities;
|
|||||||
using Wabbajack.App.ViewModels.SubViewModels;
|
using Wabbajack.App.ViewModels.SubViewModels;
|
||||||
using Wabbajack.Common;
|
using Wabbajack.Common;
|
||||||
using Wabbajack.Downloaders.GameFile;
|
using Wabbajack.Downloaders.GameFile;
|
||||||
|
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;
|
||||||
@ -44,6 +45,7 @@ public class StandardInstallationViewModel : ViewModelBase
|
|||||||
private IServiceScope _scope;
|
private IServiceScope _scope;
|
||||||
private SlideViewModel[] _slides = Array.Empty<SlideViewModel>();
|
private SlideViewModel[] _slides = Array.Empty<SlideViewModel>();
|
||||||
private Timer _slideTimer;
|
private Timer _slideTimer;
|
||||||
|
private Timer _updateTimer;
|
||||||
|
|
||||||
public StandardInstallationViewModel(ILogger<StandardInstallationViewModel> logger, IServiceProvider provider,
|
public StandardInstallationViewModel(ILogger<StandardInstallationViewModel> logger, IServiceProvider provider,
|
||||||
GameLocator locator, DTOSerializer dtos,
|
GameLocator locator, DTOSerializer dtos,
|
||||||
@ -63,6 +65,9 @@ public class StandardInstallationViewModel : ViewModelBase
|
|||||||
|
|
||||||
this.WhenActivated(disposables =>
|
this.WhenActivated(disposables =>
|
||||||
{
|
{
|
||||||
|
_updateTimer = new Timer(UpdateStatus, null, TimeSpan.FromMilliseconds(1), TimeSpan.FromMilliseconds(100));
|
||||||
|
_updateTimer.DisposeWith(disposables);
|
||||||
|
|
||||||
_slideTimer = new Timer(_ =>
|
_slideTimer = new Timer(_ =>
|
||||||
{
|
{
|
||||||
if (IsPlaying) NextSlide(1);
|
if (IsPlaying) NextSlide(1);
|
||||||
@ -103,11 +108,23 @@ public class StandardInstallationViewModel : ViewModelBase
|
|||||||
[Reactive] public Percent StepsProgress { get; set; } = Percent.Zero;
|
[Reactive] public Percent StepsProgress { get; set; } = Percent.Zero;
|
||||||
[Reactive] public Percent StepProgress { 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 = new("", Percent.Zero, Percent.Zero);
|
||||||
|
|
||||||
public void Receive(StartInstallation msg)
|
public void Receive(StartInstallation msg)
|
||||||
{
|
{
|
||||||
Install(msg).FireAndForget();
|
Install(msg).FireAndForget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateStatus(object? state)
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
StepsProgress = _latestStatus.StepsProgress;
|
||||||
|
StepProgress = _latestStatus.StepProgress;
|
||||||
|
StatusText = _latestStatus.StatusText;
|
||||||
|
}, DispatcherPriority.Render);
|
||||||
|
}
|
||||||
|
|
||||||
private void NextSlide(int direction)
|
private void NextSlide(int direction)
|
||||||
{
|
{
|
||||||
@ -175,21 +192,12 @@ public class StandardInstallationViewModel : ViewModelBase
|
|||||||
|
|
||||||
_installer = _provider.GetService<StandardInstaller>()!;
|
_installer = _provider.GetService<StandardInstaller>()!;
|
||||||
|
|
||||||
_installer.OnStatusUpdate = async update =>
|
_installer.OnStatusUpdate = update => _latestStatus = 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");
|
_logger.LogInformation("Installer created, starting the installation process");
|
||||||
try
|
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) throw new Exception("Installation failed");
|
||||||
|
|
||||||
if (result) await SaveConfigAndContinue(_config);
|
if (result) await SaveConfigAndContinue(_config);
|
||||||
@ -200,6 +208,7 @@ public class StandardInstallationViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task SaveConfigAndContinue(InstallerConfiguration config)
|
private async Task SaveConfigAndContinue(InstallerConfiguration config)
|
||||||
{
|
{
|
||||||
var path = config.Install.Combine("modlist-image.png");
|
var path = config.Install.Combine("modlist-image.png");
|
||||||
|
@ -27,11 +27,12 @@ public class InstallConfigurationViewModel : ViewModelBase, IActivatableViewMode
|
|||||||
{
|
{
|
||||||
private readonly DTOSerializer _dtos;
|
private readonly DTOSerializer _dtos;
|
||||||
private readonly InstallationStateManager _stateManager;
|
private readonly InstallationStateManager _stateManager;
|
||||||
|
private readonly SettingsManager _settingsManager;
|
||||||
|
|
||||||
|
public InstallConfigurationViewModel(DTOSerializer dtos, InstallationStateManager stateManager, SettingsManager settingsManager)
|
||||||
public InstallConfigurationViewModel(DTOSerializer dtos, InstallationStateManager stateManager)
|
|
||||||
{
|
{
|
||||||
_stateManager = stateManager;
|
_stateManager = stateManager;
|
||||||
|
_settingsManager = settingsManager;
|
||||||
|
|
||||||
_dtos = dtos;
|
_dtos = dtos;
|
||||||
Activator = new ViewModelActivator();
|
Activator = new ViewModelActivator();
|
||||||
@ -51,22 +52,22 @@ public class InstallConfigurationViewModel : ViewModelBase, IActivatableViewMode
|
|||||||
|
|
||||||
this.WhenAnyValue(t => t.ModListPath)
|
this.WhenAnyValue(t => t.ModListPath)
|
||||||
.Where(t => t != default)
|
.Where(t => t != default)
|
||||||
.SelectAsync(disposables, async x => await LoadModList(x))
|
.SelectMany(async x => await LoadModList(x))
|
||||||
.Select(x => x)
|
.OnUIThread()
|
||||||
.ObserveOn(AvaloniaScheduler.Instance)
|
.ObserveOn(AvaloniaScheduler.Instance)
|
||||||
.BindTo(this, t => t.ModList)
|
.BindTo(this, t => t.ModList)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.WhenAnyValue(t => t.ModListPath)
|
this.WhenAnyValue(t => t.ModListPath)
|
||||||
.Where(t => t != default)
|
.Where(t => t != default)
|
||||||
.SelectAsync(disposables, async x => await LoadModListImage(x))
|
.SelectMany(async x => await LoadModListImage(x))
|
||||||
.ObserveOn(AvaloniaScheduler.Instance)
|
.OnUIThread()
|
||||||
.BindTo(this, t => t.ModListImage)
|
.BindTo(this, t => t.ModListImage)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
var settings = this.WhenAnyValue(t => t.ModListPath)
|
var settings = this.WhenAnyValue(t => t.ModListPath)
|
||||||
.SelectAsync(disposables, async v => await _stateManager.Get(v))
|
.SelectMany(async v => await _stateManager.Get(v))
|
||||||
.ObserveOn(RxApp.MainThreadScheduler)
|
.OnUIThread()
|
||||||
.Where(s => s != null);
|
.Where(s => s != null);
|
||||||
|
|
||||||
settings.Select(s => s!.Install)
|
settings.Select(s => s!.Install)
|
||||||
@ -76,9 +77,24 @@ public class InstallConfigurationViewModel : ViewModelBase, IActivatableViewMode
|
|||||||
settings.Select(s => s!.Downloads)
|
settings.Select(s => s!.Downloads)
|
||||||
.BindTo(this, vm => vm.Download)
|
.BindTo(this, vm => vm.Download)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
|
|
||||||
|
LoadSettings().FireAndForget();
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task LoadSettings()
|
||||||
|
{
|
||||||
|
var path = await _settingsManager.Load<AbsolutePath>("last-install-path");
|
||||||
|
if (path != default && path.FileExists())
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() => {
|
||||||
|
ModListPath = path;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Reactive] public AbsolutePath ModListPath { get; set; }
|
[Reactive] public AbsolutePath ModListPath { get; set; }
|
||||||
|
|
||||||
[Reactive] public AbsolutePath Install { get; set; }
|
[Reactive] public AbsolutePath Install { get; set; }
|
||||||
@ -107,13 +123,15 @@ public class InstallConfigurationViewModel : ViewModelBase, IActivatableViewMode
|
|||||||
if (metadataPath.FileExists())
|
if (metadataPath.FileExists())
|
||||||
metadata = _dtos.Deserialize<ModlistMetadata>(await metadataPath.ReadAllTextAsync());
|
metadata = _dtos.Deserialize<ModlistMetadata>(await metadataPath.ReadAllTextAsync());
|
||||||
|
|
||||||
_stateManager.SetLastState(new InstallationConfigurationSetting
|
await _stateManager.SetLastState(new InstallationConfigurationSetting
|
||||||
{
|
{
|
||||||
ModList = ModListPath,
|
ModList = ModListPath,
|
||||||
Downloads = Download,
|
Downloads = Download,
|
||||||
Install = Install,
|
Install = Install,
|
||||||
Metadata = metadata
|
Metadata = metadata
|
||||||
}).FireAndForget();
|
});
|
||||||
|
|
||||||
|
await _settingsManager.Save("last-install-path", ModListPath);
|
||||||
|
|
||||||
MessageBus.Current.SendMessage(new NavigateTo(typeof(StandardInstallationViewModel)));
|
MessageBus.Current.SendMessage(new NavigateTo(typeof(StandardInstallationViewModel)));
|
||||||
MessageBus.Current.SendMessage(new StartInstallation(ModListPath, Install, Download, metadata));
|
MessageBus.Current.SendMessage(new StartInstallation(ModListPath, Install, Download, metadata));
|
||||||
|
@ -59,8 +59,8 @@ public class MainWindowViewModel : ReactiveValidationObject, IActivatableViewMod
|
|||||||
this.WhenActivated(disposables =>
|
this.WhenActivated(disposables =>
|
||||||
{
|
{
|
||||||
BackButton = ReactiveCommand.Create(() => { Receive(new NavigateBack()); },
|
BackButton = ReactiveCommand.Create(() => { Receive(new NavigateBack()); },
|
||||||
this.ObservableForProperty(vm => vm.BreadCrumbs)
|
this.WhenAnyValue(vm => vm.BreadCrumbs)
|
||||||
.Select(bc => bc.Value.Count() > 1))
|
.Select(bc => bc.Count() > 1))
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
SettingsButton = ReactiveCommand.Create(() => { Receive(new NavigateTo(typeof(SettingsViewModel))); })
|
SettingsButton = ReactiveCommand.Create(() => { Receive(new NavigateTo(typeof(SettingsViewModel))); })
|
||||||
@ -68,6 +68,13 @@ public class MainWindowViewModel : ReactiveValidationObject, IActivatableViewMod
|
|||||||
|
|
||||||
LogViewButton = ReactiveCommand.Create(() => { Receive(new NavigateTo(typeof(LogScreenViewModel))); })
|
LogViewButton = ReactiveCommand.Create(() => { Receive(new NavigateTo(typeof(LogScreenViewModel))); })
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
|
this.WhenAnyValue(vm => vm.CurrentScreen)
|
||||||
|
.Where(view => view != default)
|
||||||
|
.Select(view => ((IScreenView) view).HumanName)
|
||||||
|
.Select(txt => txt == "" ? "Wabbajack" : $"Wabbajack - {txt}")
|
||||||
|
.BindTo(this, vm => vm.TitleText)
|
||||||
|
.DisposeWith(disposables);
|
||||||
});
|
});
|
||||||
CurrentScreen = (Control) _screens.First(s => s.ViewModelType == typeof(ModeSelectionViewModel));
|
CurrentScreen = (Control) _screens.First(s => s.ViewModelType == typeof(ModeSelectionViewModel));
|
||||||
|
|
||||||
@ -86,6 +93,8 @@ public class MainWindowViewModel : ReactiveValidationObject, IActivatableViewMod
|
|||||||
|
|
||||||
[Reactive] public string ResourceStatus { get; set; }
|
[Reactive] public string ResourceStatus { get; set; }
|
||||||
|
|
||||||
|
[Reactive] public string TitleText { get; set; }
|
||||||
|
|
||||||
public ViewModelActivator Activator { get; }
|
public ViewModelActivator Activator { get; }
|
||||||
|
|
||||||
public void Receive(NavigateBack val)
|
public void Receive(NavigateBack val)
|
||||||
|
@ -8,7 +8,7 @@ namespace Wabbajack.App.Views;
|
|||||||
|
|
||||||
public partial class GuidedWebView : ScreenBase<GuidedWebViewModel>
|
public partial class GuidedWebView : ScreenBase<GuidedWebViewModel>
|
||||||
{
|
{
|
||||||
public GuidedWebView() : base(false)
|
public GuidedWebView() : base("Web View", false)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
|
@ -9,39 +9,44 @@ using Wabbajack.App.ViewModels;
|
|||||||
|
|
||||||
namespace Wabbajack.App.Views;
|
namespace Wabbajack.App.Views;
|
||||||
|
|
||||||
public partial class InstallConfigurationView : ReactiveUserControl<InstallConfigurationViewModel>, IScreenView
|
public partial class InstallConfigurationView : ScreenBase<InstallConfigurationViewModel>, IScreenView
|
||||||
{
|
{
|
||||||
public InstallConfigurationView()
|
public InstallConfigurationView() : base("Install Configuration")
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
DataContext = App.Services.GetService<InstallConfigurationViewModel>()!;
|
DataContext = App.Services.GetService<InstallConfigurationViewModel>()!;
|
||||||
|
|
||||||
this.WhenActivated(disposables =>
|
this.WhenActivated(disposables =>
|
||||||
{
|
{
|
||||||
this.Bind(ViewModel, x => x.ModListPath,
|
ViewModel.WhenAnyValue(vm => vm.ModListPath)
|
||||||
view => view.ModListFile.SelectedPath)
|
.Subscribe(path => ModListFile.Load(path))
|
||||||
.DisposeWith(disposables);
|
|
||||||
this.Bind(ViewModel, x => x.Download,
|
|
||||||
view => view.DownloadPath.SelectedPath)
|
|
||||||
.DisposeWith(disposables);
|
|
||||||
this.Bind(ViewModel, x => x.Install,
|
|
||||||
view => view.InstallPath.SelectedPath)
|
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
ViewModel.WhenAnyValue(x => x.BeginCommand)
|
ViewModel.WhenAnyValue(vm => vm.ModListPath)
|
||||||
.Where(x => x != default)
|
.Subscribe(path => ModListFile.Load(path))
|
||||||
.BindTo(BeginInstall, x => x.Button.Command)
|
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
ViewModel.WhenAnyValue(x => x.ModList)
|
ViewModel.WhenAnyValue(vm => vm.Download)
|
||||||
.Where(x => x != default)
|
.Subscribe(path => DownloadPath.Load(path))
|
||||||
.Select(x => x!.Name)
|
|
||||||
.BindTo(ModListName, x => x.Text)
|
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
ViewModel.WhenAnyValue(x => x.ModListImage)
|
ViewModel.WhenAnyValue(vm => vm.Install)
|
||||||
.Where(x => x != default)
|
.Subscribe(path => InstallPath.Load(path))
|
||||||
.BindTo(ModListImage, x => x.Source)
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
|
this.WhenAnyValue(view => view.ModListFile.SelectedPath)
|
||||||
|
.BindTo(ViewModel, vm => vm.ModListPath)
|
||||||
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
|
this.WhenAnyValue(view => view.DownloadPath.SelectedPath)
|
||||||
|
.BindTo(ViewModel, vm => vm.Download)
|
||||||
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
|
this.WhenAnyValue(view => view.InstallPath.SelectedPath)
|
||||||
|
.BindTo(ViewModel, vm => vm.Install)
|
||||||
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
|
this.BindCommand(ViewModel, vm => vm.BeginCommand, view => view.BeginInstall.Button)
|
||||||
.DisposeWith(disposables);
|
.DisposeWith(disposables);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -32,25 +32,26 @@
|
|||||||
</Design.DataContext>
|
</Design.DataContext>
|
||||||
|
|
||||||
<Grid RowDefinitions="40, *">
|
<Grid RowDefinitions="40, *">
|
||||||
<Grid ColumnDefinitions="40, *, 40, 40, 40, 40">
|
<Grid ColumnDefinitions="40, Auto, *, 40, 40, 40, 40">
|
||||||
<Button Grid.Column="0" x:Name="BackButton" x:FieldModifier="public">
|
<Button Grid.Column="0" x:Name="BackButton" x:FieldModifier="public">
|
||||||
<i:MaterialIcon Kind="ArrowBack" />
|
<i:MaterialIcon Kind="ArrowBack" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<TextBlock Grid.Column="1" HorizontalAlignment="Left" VerticalAlignment="Center" x:Name="TitleText"></TextBlock>
|
||||||
|
|
||||||
<TextBlock Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" x:Name="ResourceStatus" />
|
<TextBlock Grid.Column="2" HorizontalAlignment="Right" VerticalAlignment="Center" x:Name="ResourceStatus" />
|
||||||
|
|
||||||
<Button Grid.Column="2" x:Name="LogButton">
|
<Button Grid.Column="3" x:Name="LogButton">
|
||||||
<i:MaterialIcon Kind="ViewList" />
|
<i:MaterialIcon Kind="ViewList" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button Grid.Column="3" x:Name="SettingsButton">
|
<Button Grid.Column="4" x:Name="SettingsButton">
|
||||||
<i:MaterialIcon Kind="Gear" />
|
<i:MaterialIcon Kind="Gear" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button Grid.Column="4" x:Name="MinimizeButton">
|
<Button Grid.Column="5" x:Name="MinimizeButton">
|
||||||
<i:MaterialIcon Kind="WindowMinimize" />
|
<i:MaterialIcon Kind="WindowMinimize" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button Grid.Column="5" x:Name="CloseButton" x:FieldModifier="public">
|
<Button Grid.Column="6" x:Name="CloseButton" x:FieldModifier="public">
|
||||||
<i:MaterialIcon Kind="Close" />
|
<i:MaterialIcon Kind="Close" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
@ -16,27 +16,30 @@ public partial class MainWindow : ReactiveWindow<MainWindowViewModel>
|
|||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
DataContext = App.Services.GetService<MainWindowViewModel>()!;
|
DataContext = App.Services.GetService<MainWindowViewModel>()!;
|
||||||
|
|
||||||
this.WhenActivated(dispose =>
|
this.WhenActivated(disposables =>
|
||||||
{
|
{
|
||||||
CloseButton.Command = ReactiveCommand.Create(() => Environment.Exit(0))
|
CloseButton.Command = ReactiveCommand.Create(() => Environment.Exit(0))
|
||||||
.DisposeWith(dispose);
|
.DisposeWith(disposables);
|
||||||
MinimizeButton.Command = ReactiveCommand.Create(() => WindowState = WindowState.Minimized)
|
MinimizeButton.Command = ReactiveCommand.Create(() => WindowState = WindowState.Minimized)
|
||||||
.DisposeWith(dispose);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.BindCommand(ViewModel, vm => vm.BackButton, view => view.BackButton)
|
this.BindCommand(ViewModel, vm => vm.BackButton, view => view.BackButton)
|
||||||
.DisposeWith(dispose);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.BindCommand(ViewModel, vm => vm.SettingsButton, view => view.SettingsButton)
|
this.BindCommand(ViewModel, vm => vm.SettingsButton, view => view.SettingsButton)
|
||||||
.DisposeWith(dispose);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.BindCommand(ViewModel, vm => vm.LogViewButton, view => view.LogButton)
|
this.BindCommand(ViewModel, vm => vm.LogViewButton, view => view.LogButton)
|
||||||
.DisposeWith(dispose);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.Bind(ViewModel, vm => vm.CurrentScreen, view => view.Contents.Content)
|
this.OneWayBind(ViewModel, vm => vm.CurrentScreen, view => view.Contents.Content)
|
||||||
.DisposeWith(dispose);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
this.Bind(ViewModel, vm => vm.ResourceStatus, view => view.ResourceStatus.Text)
|
this.OneWayBind(ViewModel, vm => vm.ResourceStatus, view => view.ResourceStatus.Text)
|
||||||
.DisposeWith(dispose);
|
.DisposeWith(disposables);
|
||||||
|
|
||||||
|
this.OneWayBind(ViewModel, vm => vm.TitleText, view => view.TitleText.Text)
|
||||||
|
.DisposeWith(disposables);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ namespace Wabbajack.App.Views;
|
|||||||
|
|
||||||
public partial class ModeSelectionView : ScreenBase<ModeSelectionViewModel>
|
public partial class ModeSelectionView : ScreenBase<ModeSelectionViewModel>
|
||||||
{
|
{
|
||||||
public ModeSelectionView(IServiceProvider provider)
|
public ModeSelectionView(IServiceProvider provider) : base("")
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
this.WhenActivated(disposables =>
|
this.WhenActivated(disposables =>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using ReactiveUI.Fody.Helpers;
|
||||||
using Wabbajack.App.Interfaces;
|
using Wabbajack.App.Interfaces;
|
||||||
using Wabbajack.App.ViewModels;
|
using Wabbajack.App.ViewModels;
|
||||||
|
|
||||||
@ -7,9 +8,13 @@ namespace Wabbajack.App.Views;
|
|||||||
public abstract class ScreenBase<T> : ViewBase<T>, IScreenView
|
public abstract class ScreenBase<T> : ViewBase<T>, IScreenView
|
||||||
where T : ViewModelBase
|
where T : ViewModelBase
|
||||||
{
|
{
|
||||||
protected ScreenBase(bool createViewModel = true) : base(createViewModel)
|
protected ScreenBase(string humanName, bool createViewModel = true) : base(createViewModel)
|
||||||
{
|
{
|
||||||
|
HumanName = humanName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Type ViewModelType => typeof(T);
|
public Type ViewModelType => typeof(T);
|
||||||
|
|
||||||
|
[Reactive]
|
||||||
|
public string HumanName { get; set; }
|
||||||
}
|
}
|
@ -67,7 +67,8 @@ public class FileExtractor
|
|||||||
Predicate<RelativePath> shouldExtract,
|
Predicate<RelativePath> shouldExtract,
|
||||||
Func<RelativePath, IExtractedFile, ValueTask<T>> mapfn,
|
Func<RelativePath, IExtractedFile, ValueTask<T>> mapfn,
|
||||||
CancellationToken token,
|
CancellationToken token,
|
||||||
HashSet<RelativePath>? onlyFiles = null)
|
HashSet<RelativePath>? onlyFiles = null,
|
||||||
|
Action<Percent>? progressFunction = null)
|
||||||
{
|
{
|
||||||
if (sFn is NativeFileStreamFactory) _logger.LogInformation("Extracting {file}", sFn.Name);
|
if (sFn is NativeFileStreamFactory) _logger.LogInformation("Extracting {file}", sFn.Name);
|
||||||
await using var archive = await sFn.GetStream();
|
await using var archive = await sFn.GetStream();
|
||||||
@ -92,7 +93,7 @@ public class FileExtractor
|
|||||||
{
|
{
|
||||||
await using var tempFolder = _manager.CreateFolder();
|
await using var tempFolder = _manager.CreateFolder();
|
||||||
results = await GatheringExtractWith7Zip(sFn, shouldExtract,
|
results = await GatheringExtractWith7Zip(sFn, shouldExtract,
|
||||||
mapfn, onlyFiles, token);
|
mapfn, onlyFiles, token, progressFunction);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@ -179,7 +180,8 @@ public class FileExtractor
|
|||||||
Predicate<RelativePath> shouldExtract,
|
Predicate<RelativePath> shouldExtract,
|
||||||
Func<RelativePath, IExtractedFile, ValueTask<T>> mapfn,
|
Func<RelativePath, IExtractedFile, ValueTask<T>> mapfn,
|
||||||
IReadOnlyCollection<RelativePath>? onlyFiles,
|
IReadOnlyCollection<RelativePath>? onlyFiles,
|
||||||
CancellationToken token)
|
CancellationToken token,
|
||||||
|
Action<Percent>? progressFunction = null)
|
||||||
{
|
{
|
||||||
TemporaryPath? tmpFile = null;
|
TemporaryPath? tmpFile = null;
|
||||||
await using var dest = _manager.CreateFolder();
|
await using var dest = _manager.CreateFolder();
|
||||||
@ -262,6 +264,9 @@ public class FileExtractor
|
|||||||
var newPosition = percentInt == 0 ? 0 : totalSize / percentInt;
|
var newPosition = percentInt == 0 ? 0 : totalSize / percentInt;
|
||||||
var throughput = newPosition - oldPosition;
|
var throughput = newPosition - oldPosition;
|
||||||
job.ReportNoWait((int) throughput);
|
job.ReportNoWait((int) throughput);
|
||||||
|
|
||||||
|
progressFunction?.Invoke(Percent.FactoryPutInRange(lastPercent, 100));
|
||||||
|
|
||||||
lastPercent = percentInt;
|
lastPercent = percentInt;
|
||||||
}, token);
|
}, token);
|
||||||
|
|
||||||
@ -306,7 +311,7 @@ public class FileExtractor
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task ExtractAll(AbsolutePath src, AbsolutePath dest, CancellationToken token,
|
public async Task ExtractAll(AbsolutePath src, AbsolutePath dest, CancellationToken token,
|
||||||
Predicate<RelativePath>? filterFn = null)
|
Predicate<RelativePath>? filterFn = null, Action<Percent>? updateProgress = null)
|
||||||
{
|
{
|
||||||
filterFn ??= _ => true;
|
filterFn ??= _ => true;
|
||||||
await GatheringExtract(new NativeFileStreamFactory(src), filterFn, async (path, factory) =>
|
await GatheringExtract(new NativeFileStreamFactory(src), filterFn, async (path, factory) =>
|
||||||
@ -316,6 +321,6 @@ public class FileExtractor
|
|||||||
await using var stream = await factory.GetStream();
|
await using var stream = await factory.GetStream();
|
||||||
await abs.WriteAllAsync(stream, token);
|
await abs.WriteAllAsync(stream, token);
|
||||||
return 0;
|
return 0;
|
||||||
}, token);
|
}, token, progressFunction: updateProgress);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -51,11 +51,11 @@ public abstract class AInstaller<T>
|
|||||||
private long _currentStepProgress;
|
private long _currentStepProgress;
|
||||||
|
|
||||||
|
|
||||||
private long _maxStepProgress;
|
protected long MaxStepProgress { get; set; }
|
||||||
private string _statusText;
|
private string _statusText;
|
||||||
private readonly Stopwatch _updateStopWatch = new();
|
private readonly Stopwatch _updateStopWatch = new();
|
||||||
|
|
||||||
public Func<StatusUpdate, Task>? OnStatusUpdate;
|
public Action<StatusUpdate>? OnStatusUpdate;
|
||||||
|
|
||||||
|
|
||||||
public AInstaller(ILogger<T> logger, InstallerConfiguration config, IGameLocator gameLocator,
|
public AInstaller(ILogger<T> logger, InstallerConfiguration config, IGameLocator gameLocator,
|
||||||
@ -87,41 +87,42 @@ public abstract class AInstaller<T>
|
|||||||
|
|
||||||
public ModList ModList => _configuration.ModList;
|
public ModList ModList => _configuration.ModList;
|
||||||
|
|
||||||
public async Task NextStep(string statusText, long maxStepProgress)
|
public void NextStep(string statusText, long maxStepProgress)
|
||||||
{
|
{
|
||||||
_updateStopWatch.Restart();
|
_updateStopWatch.Restart();
|
||||||
_maxStepProgress = maxStepProgress;
|
MaxStepProgress = maxStepProgress;
|
||||||
_currentStep += 1;
|
_currentStep += 1;
|
||||||
_statusText = statusText;
|
_statusText = statusText;
|
||||||
_logger.LogInformation("Next Step: {Step}", statusText);
|
_logger.LogInformation("Next Step: {Step}", statusText);
|
||||||
|
|
||||||
if (OnStatusUpdate != null)
|
OnStatusUpdate?.Invoke(new StatusUpdate($"[{_currentStep}/{MaxSteps}] " + statusText,
|
||||||
await OnStatusUpdate!(new StatusUpdate($"[{_currentStep}/{MaxSteps}] " + statusText,
|
|
||||||
Percent.FactoryPutInRange(_currentStep, MaxSteps), Percent.Zero));
|
Percent.FactoryPutInRange(_currentStep, MaxSteps), Percent.Zero));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask UpdateProgress(long stepProgress)
|
public void UpdateProgress(long stepProgress)
|
||||||
{
|
{
|
||||||
Interlocked.Add(ref _currentStepProgress, stepProgress);
|
Interlocked.Add(ref _currentStepProgress, stepProgress);
|
||||||
|
|
||||||
if (_updateStopWatch.ElapsedMilliseconds < _limitMS) return;
|
OnStatusUpdate?.Invoke(new StatusUpdate($"[{_currentStep}/{MaxSteps}] " + _statusText, Percent.FactoryPutInRange(_currentStep, MaxSteps),
|
||||||
lock (_updateStopWatch)
|
Percent.FactoryPutInRange(_currentStepProgress, MaxStepProgress)));
|
||||||
{
|
|
||||||
if (_updateStopWatch.ElapsedMilliseconds < _limitMS) return;
|
|
||||||
_updateStopWatch.Restart();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (OnStatusUpdate != null)
|
|
||||||
await OnStatusUpdate!(new StatusUpdate(_statusText, Percent.FactoryPutInRange(_currentStep, MaxSteps),
|
|
||||||
Percent.FactoryPutInRange(_currentStepProgress, _maxStepProgress)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract Task<bool> Begin(CancellationToken token);
|
public abstract Task<bool> Begin(CancellationToken token);
|
||||||
|
|
||||||
public async Task ExtractModlist(CancellationToken token)
|
protected async Task ExtractModlist(CancellationToken token)
|
||||||
{
|
{
|
||||||
ExtractedModlistFolder = _manager.CreateFolder();
|
ExtractedModlistFolder = _manager.CreateFolder();
|
||||||
await _extractor.ExtractAll(_configuration.ModlistArchive, ExtractedModlistFolder, token);
|
await using var stream = _configuration.ModlistArchive.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
using var archive = new ZipArchive(stream, ZipArchiveMode.Read);
|
||||||
|
NextStep("Extracting Modlist", archive.Entries.Count);
|
||||||
|
foreach (var entry in archive.Entries)
|
||||||
|
{
|
||||||
|
var path = entry.FullName.ToRelativePath().RelativeTo(ExtractedModlistFolder);
|
||||||
|
path.Parent.CreateDirectory();
|
||||||
|
await using var of = path.Open(FileMode.Create, FileAccess.Write, FileShare.None);
|
||||||
|
await entry.Open().CopyToAsync(of, token);
|
||||||
|
UpdateProgress(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<byte[]> LoadBytesFromPath(RelativePath path)
|
public async Task<byte[]> LoadBytesFromPath(RelativePath path)
|
||||||
@ -165,13 +166,15 @@ public abstract class AInstaller<T>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected async Task PrimeVFS()
|
protected async Task PrimeVFS()
|
||||||
{
|
{
|
||||||
|
NextStep("Priming VFS", 0);
|
||||||
_vfs.AddKnown(_configuration.ModList.Directives.OfType<FromArchive>().Select(d => d.ArchiveHashPath),
|
_vfs.AddKnown(_configuration.ModList.Directives.OfType<FromArchive>().Select(d => d.ArchiveHashPath),
|
||||||
HashedArchives);
|
HashedArchives);
|
||||||
await _vfs.BackfillMissing();
|
await _vfs.BackfillMissing();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void BuildFolderStructure()
|
public async Task BuildFolderStructure()
|
||||||
{
|
{
|
||||||
|
NextStep("Building Folder Structure", 0);
|
||||||
_logger.LogInformation("Building Folder Structure");
|
_logger.LogInformation("Building Folder Structure");
|
||||||
ModList.Directives
|
ModList.Directives
|
||||||
.Where(d => d.To.Depth > 1)
|
.Where(d => d.To.Depth > 1)
|
||||||
@ -182,7 +185,7 @@ public abstract class AInstaller<T>
|
|||||||
|
|
||||||
public async Task InstallArchives(CancellationToken token)
|
public async Task InstallArchives(CancellationToken token)
|
||||||
{
|
{
|
||||||
await NextStep("Installing files", ModList.Directives.Sum(d => d.Size));
|
NextStep("Installing files", ModList.Directives.Sum(d => d.Size));
|
||||||
var grouped = ModList.Directives
|
var grouped = ModList.Directives
|
||||||
.OfType<FromArchive>()
|
.OfType<FromArchive>()
|
||||||
.Select(a => new {VF = _vfs.Index.FileForArchiveHashPath(a.ArchiveHashPath), Directive = a})
|
.Select(a => new {VF = _vfs.Index.FileForArchiveHashPath(a.ArchiveHashPath), Directive = a})
|
||||||
@ -196,7 +199,7 @@ public abstract class AInstaller<T>
|
|||||||
foreach (var directive in grouped[vf])
|
foreach (var directive in grouped[vf])
|
||||||
{
|
{
|
||||||
var file = directive.Directive;
|
var file = directive.Directive;
|
||||||
await UpdateProgress(file.Size);
|
UpdateProgress(file.Size);
|
||||||
|
|
||||||
switch (file)
|
switch (file)
|
||||||
{
|
{
|
||||||
@ -284,7 +287,7 @@ public abstract class AInstaller<T>
|
|||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Downloading {count} archives", missing.Count);
|
_logger.LogInformation("Downloading {count} archives", missing.Count);
|
||||||
await NextStep("Downloading files", missing.Count);
|
NextStep("Downloading files", missing.Count);
|
||||||
|
|
||||||
await missing
|
await missing
|
||||||
.OrderBy(a => a.Size)
|
.OrderBy(a => a.Size)
|
||||||
@ -305,7 +308,7 @@ public abstract class AInstaller<T>
|
|||||||
}
|
}
|
||||||
|
|
||||||
await DownloadArchive(archive, download, token, outputPath);
|
await DownloadArchive(archive, download, token, outputPath);
|
||||||
await UpdateProgress(1);
|
UpdateProgress(1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -345,6 +348,7 @@ public abstract class AInstaller<T>
|
|||||||
|
|
||||||
public async Task HashArchives(CancellationToken token)
|
public async Task HashArchives(CancellationToken token)
|
||||||
{
|
{
|
||||||
|
NextStep("Hashing Archives", 0);
|
||||||
_logger.LogInformation("Looking for files to hash");
|
_logger.LogInformation("Looking for files to hash");
|
||||||
|
|
||||||
var allFiles = _configuration.Downloads.EnumerateFiles()
|
var allFiles = _configuration.Downloads.EnumerateFiles()
|
||||||
@ -356,12 +360,18 @@ public abstract class AInstaller<T>
|
|||||||
var toHash = ModList.Archives.Where(a => hashDict.ContainsKey(a.Size))
|
var toHash = ModList.Archives.Where(a => hashDict.ContainsKey(a.Size))
|
||||||
.SelectMany(a => hashDict[a.Size]).ToList();
|
.SelectMany(a => hashDict[a.Size]).ToList();
|
||||||
|
|
||||||
|
MaxStepProgress = toHash.Count;
|
||||||
|
|
||||||
_logger.LogInformation("Found {count} total files, {hashedCount} matching filesize", allFiles.Count,
|
_logger.LogInformation("Found {count} total files, {hashedCount} matching filesize", allFiles.Count,
|
||||||
toHash.Count);
|
toHash.Count);
|
||||||
|
|
||||||
var hashResults = await
|
var hashResults = await
|
||||||
toHash
|
toHash
|
||||||
.PMapAll(async e => (await _fileHashCache.FileHashCachedAsync(e, token), e))
|
.PMapAll(async e =>
|
||||||
|
{
|
||||||
|
UpdateProgress(1);
|
||||||
|
return (await _fileHashCache.FileHashCachedAsync(e, token), e);
|
||||||
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
HashedArchives = hashResults
|
HashedArchives = hashResults
|
||||||
@ -389,11 +399,11 @@ public abstract class AInstaller<T>
|
|||||||
var savePath = (RelativePath) "saves";
|
var savePath = (RelativePath) "saves";
|
||||||
|
|
||||||
var existingFiles = _configuration.Install.EnumerateFiles().ToList();
|
var existingFiles = _configuration.Install.EnumerateFiles().ToList();
|
||||||
await NextStep("Optimizing Modlist: Looking for files to delete", existingFiles.Count);
|
NextStep("Optimizing Modlist: Looking for files to delete", existingFiles.Count);
|
||||||
await existingFiles
|
await existingFiles
|
||||||
.PDoAll(async f =>
|
.PDoAll(async f =>
|
||||||
{
|
{
|
||||||
await UpdateProgress(1);
|
UpdateProgress(1);
|
||||||
var relativeTo = f.RelativeTo(_configuration.Install);
|
var relativeTo = f.RelativeTo(_configuration.Install);
|
||||||
if (indexed.ContainsKey(relativeTo) || f.InFolder(_configuration.Downloads))
|
if (indexed.ContainsKey(relativeTo) || f.InFolder(_configuration.Downloads))
|
||||||
return;
|
return;
|
||||||
@ -408,12 +418,12 @@ public abstract class AInstaller<T>
|
|||||||
});
|
});
|
||||||
|
|
||||||
_logger.LogInformation("Cleaning empty folders");
|
_logger.LogInformation("Cleaning empty folders");
|
||||||
await NextStep("Optimizing Modlist: Cleaning empty folders", indexed.Keys.Count);
|
NextStep("Optimizing Modlist: Cleaning empty folders", indexed.Keys.Count);
|
||||||
var expectedFolders = (await indexed.Keys
|
var expectedFolders = (indexed.Keys
|
||||||
.Select(f => f.RelativeTo(_configuration.Install))
|
.Select(f => f.RelativeTo(_configuration.Install))
|
||||||
// We ignore the last part of the path, so we need a dummy file name
|
// We ignore the last part of the path, so we need a dummy file name
|
||||||
.Append(_configuration.Downloads.Combine("_"))
|
.Append(_configuration.Downloads.Combine("_"))
|
||||||
.OnEach(async _ => await UpdateProgress(1))
|
.OnEach(_ => UpdateProgress(1))
|
||||||
.Where(f => f.InFolder(_configuration.Install))
|
.Where(f => f.InFolder(_configuration.Install))
|
||||||
.SelectMany(path =>
|
.SelectMany(path =>
|
||||||
{
|
{
|
||||||
@ -443,10 +453,9 @@ public abstract class AInstaller<T>
|
|||||||
|
|
||||||
var existingfiles = _configuration.Install.EnumerateFiles().ToHashSet();
|
var existingfiles = _configuration.Install.EnumerateFiles().ToHashSet();
|
||||||
|
|
||||||
await NextStep("Optimizing Modlist: Removing redundant directives", indexed.Count);
|
NextStep("Optimizing Modlist: Removing redundant directives", indexed.Count);
|
||||||
await indexed.Values.PMapAll<Directive, Directive?>(async d =>
|
await indexed.Values.PMapAll<Directive, Directive?>(async d =>
|
||||||
{
|
{
|
||||||
await UpdateProgress(1);
|
|
||||||
// Bit backwards, but we want to return null for
|
// Bit backwards, but we want to return null for
|
||||||
// all files we *want* installed. We return the files
|
// all files we *want* installed. We return the files
|
||||||
// to remove from the install list.
|
// to remove from the install list.
|
||||||
@ -457,11 +466,13 @@ public abstract class AInstaller<T>
|
|||||||
})
|
})
|
||||||
.Do(d =>
|
.Do(d =>
|
||||||
{
|
{
|
||||||
|
UpdateProgress(1);
|
||||||
if (d != null) indexed.Remove(d.To);
|
if (d != null) indexed.Remove(d.To);
|
||||||
});
|
});
|
||||||
|
|
||||||
_logger.LogInformation("Optimized {optimized} directives to {indexed} required", ModList.Directives.Length,
|
_logger.LogInformation("Optimized {optimized} directives to {indexed} required", ModList.Directives.Length,
|
||||||
indexed.Count);
|
indexed.Count);
|
||||||
|
NextStep("Finalizing modlist optimization", 0);
|
||||||
var requiredArchives = indexed.Values.OfType<FromArchive>()
|
var requiredArchives = indexed.Values.OfType<FromArchive>()
|
||||||
.GroupBy(d => d.ArchiveHashPath.Hash)
|
.GroupBy(d => d.ArchiveHashPath.Hash)
|
||||||
.Select(d => d.Key)
|
.Select(d => d.Key)
|
||||||
|
@ -38,13 +38,14 @@ public class StandardInstaller : AInstaller<StandardInstaller>
|
|||||||
base(logger, config, gameLocator, extractor, jsonSerializer, vfs, fileHashCache, downloadDispatcher,
|
base(logger, config, gameLocator, extractor, jsonSerializer, vfs, fileHashCache, downloadDispatcher,
|
||||||
parallelOptions, wjClient)
|
parallelOptions, wjClient)
|
||||||
{
|
{
|
||||||
MaxSteps = 7;
|
MaxSteps = 14;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<bool> Begin(CancellationToken token)
|
public override async Task<bool> Begin(CancellationToken token)
|
||||||
{
|
{
|
||||||
if (token.IsCancellationRequested) return false;
|
if (token.IsCancellationRequested) return false;
|
||||||
await _wjClient.SendMetric(MetricNames.BeginInstall, ModList.Name);
|
await _wjClient.SendMetric(MetricNames.BeginInstall, ModList.Name);
|
||||||
|
NextStep("Configuring Installer", 0);
|
||||||
_logger.LogInformation("Configuring Processor");
|
_logger.LogInformation("Configuring Processor");
|
||||||
|
|
||||||
if (_configuration.GameFolder == default)
|
if (_configuration.GameFolder == default)
|
||||||
@ -85,7 +86,6 @@ public class StandardInstaller : AInstaller<StandardInstaller>
|
|||||||
|
|
||||||
await OptimizeModlist(token);
|
await OptimizeModlist(token);
|
||||||
|
|
||||||
|
|
||||||
await HashArchives(token);
|
await HashArchives(token);
|
||||||
|
|
||||||
await DownloadArchives(token);
|
await DownloadArchives(token);
|
||||||
@ -106,7 +106,7 @@ public class StandardInstaller : AInstaller<StandardInstaller>
|
|||||||
|
|
||||||
await PrimeVFS();
|
await PrimeVFS();
|
||||||
|
|
||||||
BuildFolderStructure();
|
await BuildFolderStructure();
|
||||||
|
|
||||||
await InstallArchives(token);
|
await InstallArchives(token);
|
||||||
|
|
||||||
@ -129,7 +129,7 @@ public class StandardInstaller : AInstaller<StandardInstaller>
|
|||||||
await ExtractedModlistFolder!.DisposeAsync();
|
await ExtractedModlistFolder!.DisposeAsync();
|
||||||
await _wjClient.SendMetric(MetricNames.FinishInstall, ModList.Name);
|
await _wjClient.SendMetric(MetricNames.FinishInstall, ModList.Name);
|
||||||
|
|
||||||
await NextStep("Finished", 1);
|
NextStep("Finished", 1);
|
||||||
_logger.LogInformation("Finished Installation");
|
_logger.LogInformation("Finished Installation");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -259,12 +259,12 @@ public class StandardInstaller : AInstaller<StandardInstaller>
|
|||||||
private async Task InstallIncludedFiles(CancellationToken token)
|
private async Task InstallIncludedFiles(CancellationToken token)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Writing inline files");
|
_logger.LogInformation("Writing inline files");
|
||||||
await NextStep("Installing Included Files", ModList.Directives.OfType<InlineFile>().Count());
|
NextStep("Installing Included Files", ModList.Directives.OfType<InlineFile>().Count());
|
||||||
await ModList.Directives
|
await ModList.Directives
|
||||||
.OfType<InlineFile>()
|
.OfType<InlineFile>()
|
||||||
.PDoAll(async directive =>
|
.PDoAll(async directive =>
|
||||||
{
|
{
|
||||||
await UpdateProgress(1);
|
UpdateProgress(1);
|
||||||
var outPath = _configuration.Install.Combine(directive.To);
|
var outPath = _configuration.Install.Combine(directive.To);
|
||||||
outPath.Delete();
|
outPath.Delete();
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ public readonly struct Percent : IComparable, IEquatable<Percent>
|
|||||||
|
|
||||||
public static bool InRange(double d)
|
public static bool InRange(double d)
|
||||||
{
|
{
|
||||||
return d >= 0 || d <= 1;
|
return d is >= 0 or <= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Percent operator +(Percent c1, Percent c2)
|
public static Percent operator +(Percent c1, Percent c2)
|
||||||
|
@ -172,7 +172,7 @@ public class Context
|
|||||||
_knownArchives.TryAdd(key, value);
|
_knownArchives.TryAdd(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task BackfillMissing()
|
public async ValueTask BackfillMissing()
|
||||||
{
|
{
|
||||||
var newFiles = _knownArchives.ToDictionary(kv => kv.Key,
|
var newFiles = _knownArchives.ToDictionary(kv => kv.Key,
|
||||||
kv => new VirtualFile
|
kv => new VirtualFile
|
||||||
|
Loading…
Reference in New Issue
Block a user