0
0
mirror of https://github.com/sp-tarkov/patcher.git synced 2025-02-12 17:10:46 -05:00

Replace ViewNavigator with ReactiveUI RouterState, Fix PatchHelper rasing progress changed with no remaining source files, fix view load race conditions

This commit is contained in:
IsWaffle 2021-12-28 19:44:25 -05:00
parent 6ef883958d
commit 25a3c270be
22 changed files with 264 additions and 93 deletions

View File

@ -1,14 +0,0 @@
using ReactiveUI;
namespace PatchClient.Models
{
public class ViewNavigator : ReactiveObject
{
private object _SelectedViewModel;
public object SelectedViewModel
{
get => _SelectedViewModel;
set => this.RaiseAndSetIfChanged(ref _SelectedViewModel, value);
}
}
}

View File

@ -1,6 +1,9 @@
using Avalonia;
using Avalonia.ReactiveUI;
using Splat;
using System;
using ReactiveUI;
using System.Reflection;
namespace PatchClient
{
@ -15,9 +18,14 @@ namespace PatchClient
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
{
Locator.CurrentMutable.RegisterViewsForViewModels(Assembly.GetExecutingAssembly());
return AppBuilder.Configure<App>()
.UseReactiveUI()
.UsePlatformDetect()
.LogToTrace()
.UseReactiveUI();
}
}
}

View File

@ -1,13 +1,15 @@
using Avalonia;
using PatchClient.Models;
using ReactiveUI;
using Splat;
using System.Reactive.Disposables;
using System.Windows.Input;
namespace PatchClient.ViewModels
{
public class MainWindowViewModel : ViewModelBase
public class MainWindowViewModel : ReactiveObject, IActivatableViewModel, IScreen
{
public ViewModelActivator Activator { get; } = new ViewModelActivator();
public RoutingState Router { get; } = new RoutingState();
public ICommand CloseCommand => ReactiveCommand.Create(() =>
{
if (Application.Current.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktopApp)
@ -16,12 +18,12 @@ namespace PatchClient.ViewModels
}
});
public ViewNavigator navigator { get; set; } = new ViewNavigator();
public MainWindowViewModel()
{
navigator.SelectedViewModel = new PatcherViewModel();
Locator.CurrentMutable.RegisterConstant(navigator, typeof(ViewNavigator));
this.WhenActivated((CompositeDisposable disposable) =>
{
Router.Navigate.Execute(new PatcherViewModel(this));
});
}
}
}

View File

@ -4,14 +4,14 @@ namespace PatchClient.ViewModels
{
public class MessageViewModel : ViewModelBase
{
private string _InfoText;
private string _InfoText = "";
public string InfoText
{
get => _InfoText;
set => this.RaiseAndSetIfChanged(ref _InfoText, value);
}
public MessageViewModel(string Message)
public MessageViewModel(IScreen Host, string Message) : base(Host)
{
InfoText = Message;
}

View File

@ -1,11 +1,12 @@
using Avalonia;
using Avalonia.Threading;
using PatchClient.Models;
using PatcherUtils;
using ReactiveUI;
using Splat;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Disposables;
using System.Threading.Tasks;
namespace PatchClient.ViewModels
@ -16,7 +17,7 @@ namespace PatchClient.ViewModels
public ObservableCollection<LineItemProgress> LineItems { get; set; } = new ObservableCollection<LineItemProgress>();
private string _ProgressMessage;
private string _ProgressMessage = "";
public string ProgressMessage
{
get => _ProgressMessage;
@ -30,35 +31,67 @@ namespace PatchClient.ViewModels
set => this.RaiseAndSetIfChanged(ref _PatchPercent, value);
}
private string _PatchMessage;
private string _PatchMessage = "";
public string PatchMessage
{
get => _PatchMessage;
set => this.RaiseAndSetIfChanged(ref _PatchMessage, value);
}
private ViewNavigator navigator => Locator.Current.GetService<ViewNavigator>();
public PatcherViewModel()
public PatcherViewModel(IScreen Host) : base(Host)
{
RunPatcher();
this.WhenActivated((CompositeDisposable disposables) =>
{
//Test();
RunPatcher();
});
}
/// <summary>
/// A dumb testing method to see if things look right. Obsolete is used more like a warning here.
/// </summary>
[Obsolete]
private void Test()
{
Task.Run(async () =>
{
LineItem x = new LineItem("test 1", 30);
LineItem xx = new LineItem("test 2", 100);
LineItem xxx = new LineItem("test 3", 70);
LineItems.Add(new LineItemProgress(x));
LineItems.Add(new LineItemProgress(xx));
LineItems.Add(new LineItemProgress(xxx));
for (int i = 0; i <= 100; i++)
{
System.Threading.Thread.Sleep(20);
PatchPercent = i;
ProgressMessage = $"Patching @ {i}%";
foreach (var item in LineItems)
{
item.UpdateProgress(item.Total - i);
}
}
await NavigateToWithDelay(new MessageViewModel(HostScreen, "Test Complete"), 400);
});
}
private void RunPatcher()
{
Task.Run(() =>
Task.Run(async() =>
{
//Slight delay to avoid some weird race condition in avalonia core, seems to be a bug, but also maybe I'm just stupid, idk -waffle
//Error without delay: An item with the same key has already been added. Key: [1, Avalonia.Controls.Generators.ItemContainerInfo]
System.Threading.Thread.Sleep(1000);
PatchHelper patcher = new PatchHelper(Environment.CurrentDirectory, null, LazyOperations.PatchFolder);
patcher.ProgressChanged += patcher_ProgressChanged;
string message = patcher.ApplyPatches();
navigator.SelectedViewModel = new MessageViewModel(message).WithDelay(400);
await NavigateToWithDelay(new MessageViewModel(HostScreen, message), 400);
});
}

View File

@ -1,22 +1,72 @@
using PatchClient.Models;
using Avalonia.Threading;
using ReactiveUI;
using System;
using System.Threading.Tasks;
namespace PatchClient.ViewModels
{
public class ViewModelBase : ReactiveObject
public class ViewModelBase : ReactiveObject, IActivatableViewModel, IRoutableViewModel
{
public ViewModelActivator Activator { get; } = new ViewModelActivator();
public string? UrlPathSegment => Guid.NewGuid().ToString().Substring(0, 7);
public IScreen HostScreen { get; }
/// <summary>
/// Delay the return of the viewmodel
/// </summary>
/// <param name="Milliseconds">The amount of time in milliseconds to delay</param>
/// <returns>The viewmodel after the delay time</returns>
/// <remarks>Useful to delay the navigation to another view via the <see cref="ViewNavigator"/>. For instance, to allow an animation to complete.</remarks>
public ViewModelBase WithDelay(int Milliseconds)
/// <remarks>Useful to delay the navigation to another view. For instance, to allow an animation to complete.</remarks>
private async Task<ViewModelBase> WithDelay(int Milliseconds)
{
System.Threading.Thread.Sleep(Milliseconds);
await Task.Delay(Milliseconds);
return this;
}
/// <summary>
/// Navigate to another viewmodel after a delay
/// </summary>
/// <param name="ViewModel"></param>
/// <param name="Milliseconds"></param>
/// <returns></returns>
public async Task NavigateToWithDelay(ViewModelBase ViewModel, int Milliseconds)
{
await Dispatcher.UIThread.InvokeAsync(async () =>
{
HostScreen.Router.Navigate.Execute(await ViewModel.WithDelay(Milliseconds));
});
}
/// <summary>
/// Navigate to another viewmodel
/// </summary>
/// <param name="ViewModel"></param>
public void NavigateTo(ViewModelBase ViewModel)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
HostScreen.Router.Navigate.Execute(ViewModel);
});
}
/// <summary>
/// Navigate to the previous viewmodel
/// </summary>
public void NavigateBack()
{
Dispatcher.UIThread.InvokeAsync(() =>
{
HostScreen.Router.NavigateBack.Execute();
});
}
public ViewModelBase(IScreen Host)
{
HostScreen = Host;
}
}
}

View File

@ -4,6 +4,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:cc="using:PatchClient.CustomControls"
xmlns:rxui="using:Avalonia.ReactiveUI"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="PatchClient.Views.MainWindow"
Icon="/Assets/avalonia-logo.ico"
@ -29,9 +30,7 @@
<cc:TitleBar Title="Patch Client"
XButtonCommand="{Binding CloseCommand}"/>
<DockPanel LastChildFill="True" Grid.Row="1">
<ContentControl Content="{Binding navigator.SelectedViewModel}"/>
</DockPanel>
<rxui:RoutedViewHost Router="{Binding Router}" Grid.Row="1"/>
</Grid>
</Window>

View File

@ -1,10 +1,12 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using PatchClient.ViewModels;
using ReactiveUI;
namespace PatchClient.Views
{
public partial class MainWindow : Window
public partial class MainWindow : ReactiveWindow<MainWindowViewModel>
{
public MainWindow()
{
@ -16,6 +18,7 @@ namespace PatchClient.Views
private void InitializeComponent()
{
this.WhenActivated(disposables => { });
AvaloniaXamlLoader.Load(this);
}
}

View File

@ -1,9 +1,11 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using PatchClient.ViewModels;
using ReactiveUI;
namespace PatchClient.Views
{
public partial class MessageView : UserControl
public partial class MessageView : ReactiveUserControl<MessageViewModel>
{
public MessageView()
{
@ -12,6 +14,7 @@ namespace PatchClient.Views
private void InitializeComponent()
{
this.WhenActivated(disposables => { });
AvaloniaXamlLoader.Load(this);
}
}

View File

@ -1,9 +1,11 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using PatchClient.ViewModels;
using ReactiveUI;
namespace PatchClient.Views
{
public partial class PatcherView : UserControl
public partial class PatcherView : ReactiveUserControl<PatcherViewModel>
{
public PatcherView()
{
@ -12,6 +14,7 @@ namespace PatchClient.Views
private void InitializeComponent()
{
this.WhenActivated(disposables => { });
AvaloniaXamlLoader.Load(this);
}
}

View File

@ -1,6 +1,9 @@
using Avalonia;
using Avalonia.ReactiveUI;
using Splat;
using System;
using ReactiveUI;
using System.Reflection;
namespace PatchGenerator
{
@ -15,9 +18,14 @@ namespace PatchGenerator
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
{
Locator.CurrentMutable.RegisterViewsForViewModels(Assembly.GetExecutingAssembly());
return AppBuilder.Configure<App>()
.UseReactiveUI()
.UsePlatformDetect()
.LogToTrace()
.UseReactiveUI();
}
}
}

View File

@ -1,9 +1,7 @@
{
"profiles": {
"PatchGenerator": {
"commandName": "Project",
"commandLineArgs": "\"OutputFolderName::Patcher_12.3.4.5_to_6.7.8.9\" \"SourceFolderPath::C:\\Users\\JohnO\\Desktop\\12.12.10.16338\" \"TargetFolderPath::C:\\Users\\JohnO\\Desktop\\12.11.7.15680\" \"AutoZip::True\"",
"workingDirectory": "C:\\Users\\JohnO\\Desktop\\Patcher\\"
"commandName": "Project"
}
}
}

View File

@ -1,14 +1,17 @@
using Avalonia;
using PatchGenerator.Models;
using ReactiveUI;
using Splat;
using System.Windows.Input;
using System.Reactive;
using System.Reactive.Disposables;
namespace PatchGenerator.ViewModels
{
public class MainWindowViewModel : ViewModelBase
public class MainWindowViewModel : ReactiveObject, IActivatableViewModel, IScreen
{
public ICommand CloseCommand => ReactiveCommand.Create(() =>
public RoutingState Router { get; } = new RoutingState();
public ViewModelActivator Activator { get; } = new ViewModelActivator();
public ReactiveCommand<Unit, Unit> CloseCommand => ReactiveCommand.Create(() =>
{
if (Application.Current.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktopApp)
{
@ -16,25 +19,26 @@ namespace PatchGenerator.ViewModels
}
});
public ViewNavigator navigator { get; set; } = new ViewNavigator();
public MainWindowViewModel(GenStartupArgs genArgs = null)
{
Locator.CurrentMutable.RegisterConstant(navigator, typeof(ViewNavigator));
if (genArgs != null && genArgs.ReadyToRun)
this.WhenActivated((CompositeDisposable disposables) =>
{
PatchGenInfo genInfo = new PatchGenInfo();
genInfo.TargetFolderPath = genArgs.TargetFolderPath;
genInfo.SourceFolderPath = genArgs.SourceFolderPath;
genInfo.PatchName = genArgs.OutputFolderName;
genInfo.AutoZip = genArgs.AutoZip;
if (genArgs != null && genArgs.ReadyToRun)
{
PatchGenInfo genInfo = new PatchGenInfo();
navigator.SelectedViewModel = new PatchGenerationViewModel(genInfo);
return;
}
genInfo.TargetFolderPath = genArgs.TargetFolderPath;
genInfo.SourceFolderPath = genArgs.SourceFolderPath;
genInfo.PatchName = genArgs.OutputFolderName;
genInfo.AutoZip = genArgs.AutoZip;
navigator.SelectedViewModel = new OptionsViewModel();
Router.Navigate.Execute(new PatchGenerationViewModel(this, genInfo));
return;
}
Router.Navigate.Execute(new OptionsViewModel(this));
});
}
}
}

View File

@ -1,5 +1,5 @@
using PatchGenerator.Models;
using Splat;
using ReactiveUI;
namespace PatchGenerator.ViewModels
{
@ -7,9 +7,7 @@ namespace PatchGenerator.ViewModels
{
public PatchGenInfo GenerationInfo { get; set; } = new PatchGenInfo();
private ViewNavigator navigator => Locator.Current.GetService<ViewNavigator>();
public OptionsViewModel()
public OptionsViewModel(IScreen Host) : base(Host)
{
GenerationInfo.SourceFolderPath = "Drop SOURCE folder here";
GenerationInfo.TargetFolderPath = "Drop TARGET folder here";
@ -17,7 +15,7 @@ namespace PatchGenerator.ViewModels
public void GeneratePatches()
{
navigator.SelectedViewModel = new PatchGenerationViewModel(GenerationInfo);
NavigateTo(new PatchGenerationViewModel(HostScreen, GenerationInfo));
}
}
}

View File

@ -8,6 +8,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Reactive.Disposables;
using System.Text;
using System.Threading.Tasks;
@ -42,7 +43,7 @@ namespace PatchGenerator.ViewModels
private Stopwatch patchGenStopwatch = new Stopwatch();
private readonly PatchGenInfo generationInfo;
public PatchGenerationViewModel(PatchGenInfo GenerationInfo)
public PatchGenerationViewModel(IScreen Host, PatchGenInfo GenerationInfo) : base(Host)
{
generationInfo = GenerationInfo;
@ -55,17 +56,16 @@ namespace PatchGenerator.ViewModels
});
}
GeneratePatches();
this.WhenActivated((CompositeDisposable dissposables) =>
{
GeneratePatches();
});
}
public void GeneratePatches()
{
Task.Run(() =>
{
//Slight delay to avoid some weird race condition in avalonia core, seems to be a bug, but also maybe I'm just stupid, idk -waffle
//Error without delay: An item with the same key has already been added. Key: [1, Avalonia.Controls.Generators.ItemContainerInfo]
System.Threading.Thread.Sleep(1000);
string patchOutputFolder = Path.Join(generationInfo.PatchName.FromCwd(), LazyOperations.PatchFolder);
PatchHelper patcher = new PatchHelper(generationInfo.SourceFolderPath, generationInfo.TargetFolderPath, patchOutputFolder);

View File

@ -1,8 +1,72 @@
using Avalonia.Threading;
using ReactiveUI;
using System;
using System.Threading.Tasks;
namespace PatchGenerator.ViewModels
{
public class ViewModelBase : ReactiveObject
public class ViewModelBase : ReactiveObject, IActivatableViewModel, IRoutableViewModel
{
public ViewModelActivator Activator { get; } = new ViewModelActivator();
public string? UrlPathSegment => Guid.NewGuid().ToString().Substring(0, 7);
public IScreen HostScreen { get; }
/// <summary>
/// Delay the return of the viewmodel
/// </summary>
/// <param name="Milliseconds">The amount of time in milliseconds to delay</param>
/// <returns>The viewmodel after the delay time</returns>
/// <remarks>Useful to delay the navigation to another view. For instance, to allow an animation to complete.</remarks>
private async Task<ViewModelBase> WithDelay(int Milliseconds)
{
await Task.Delay(Milliseconds);
return this;
}
/// <summary>
/// Navigate to another viewmodel after a delay
/// </summary>
/// <param name="ViewModel"></param>
/// <param name="Milliseconds"></param>
/// <returns></returns>
public async Task NavigateToWithDelay(ViewModelBase ViewModel, int Milliseconds)
{
await Dispatcher.UIThread.InvokeAsync(async () =>
{
HostScreen.Router.Navigate.Execute(await ViewModel.WithDelay(Milliseconds));
});
}
/// <summary>
/// Navigate to another viewmodel
/// </summary>
/// <param name="ViewModel"></param>
public void NavigateTo(ViewModelBase ViewModel)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
HostScreen.Router.Navigate.Execute(ViewModel);
});
}
/// <summary>
/// Navigate to the previous viewmodel
/// </summary>
public void NavigateBack()
{
Dispatcher.UIThread.InvokeAsync(() =>
{
HostScreen.Router.NavigateBack.Execute();
});
}
public ViewModelBase(IScreen Host)
{
HostScreen = Host;
}
}
}

View File

@ -4,6 +4,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:cc="using:PatchGenerator.CustomControls"
xmlns:rxui="using:Avalonia.ReactiveUI"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="PatchGenerator.Views.MainWindow"
Icon="/Assets/avalonia-logo.ico"
@ -28,9 +29,7 @@
<cc:TitleBar Title="Patch Generator"
XButtonCommand="{Binding CloseCommand}"/>
<DockPanel LastChildFill="True" Grid.Row="1">
<ContentControl Content="{Binding navigator.SelectedViewModel}"/>
</DockPanel>
<rxui:RoutedViewHost Router="{Binding Router}" Grid.Row="1" />
</Grid>

View File

@ -1,10 +1,12 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using PatchGenerator.ViewModels;
using ReactiveUI;
namespace PatchGenerator.Views
{
public partial class MainWindow : Window
public partial class MainWindow : ReactiveWindow<MainWindowViewModel>
{
public MainWindow()
{
@ -16,6 +18,7 @@ namespace PatchGenerator.Views
private void InitializeComponent()
{
this.WhenActivated(disposables => { });
AvaloniaXamlLoader.Load(this);
}
}

View File

@ -1,9 +1,11 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using PatchGenerator.ViewModels;
using ReactiveUI;
namespace PatchGenerator.Views
{
public partial class OptionsView : UserControl
public partial class OptionsView : ReactiveUserControl<OptionsViewModel>
{
public OptionsView()
{
@ -12,6 +14,7 @@ namespace PatchGenerator.Views
private void InitializeComponent()
{
this.WhenActivated(disposables => { });
AvaloniaXamlLoader.Load(this);
}
}

View File

@ -1,10 +1,13 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using PatchGenerator.AttachedProperties;
using PatchGenerator.ViewModels;
using ReactiveUI;
namespace PatchGenerator.Views
{
public partial class PatchGenerationView : UserControl
public partial class PatchGenerationView : ReactiveUserControl<PatchGenerationViewModel>
{
public PatchGenerationView()
{
@ -13,6 +16,7 @@ namespace PatchGenerator.Views
private void InitializeComponent()
{
this.WhenActivated(disposables => { });
AvaloniaXamlLoader.Load(this);
}

View File

@ -242,6 +242,9 @@ namespace PatcherUtils
//Any remaining source files do not exist in the target folder and can be removed.
//reset progress info
if (SourceFiles.Count == 0) return true;
RaiseProgressChanged(0, SourceFiles.Count, "Processing .del files...");
filesProcessed = 0;
fileCountTotal = SourceFiles.Count;

Binary file not shown.