Adding WPF code back in, I need to end the 2.5 codebase

This commit is contained in:
Timothy Baldridge 2022-03-13 16:47:30 -06:00
parent b43c2fbfb0
commit 8f2f3b6eab
194 changed files with 17090 additions and 0 deletions

View File

@ -0,0 +1,19 @@
<Application
x:Class="Wabbajack.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Wabbajack"
ShutdownMode="OnExplicitShutdown"
Startup="OnStartup"
Exit="OnExit">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/Dark.Purple.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
<ResourceDictionary Source="Themes\Styles.xaml" />
<ResourceDictionary Source="Themes\CustomControls.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@ -0,0 +1,102 @@
using System;
using System.Reactive.Concurrency;
using System.Reactive.Disposables;
using System.Windows;
using System.Windows.Threading;
using CefSharp.DevTools.Debugger;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ReactiveUI;
using Splat;
using Wabbajack.Common;
using Wabbajack;
using Wabbajack.DTOs;
using Wabbajack.LoginManagers;
using Wabbajack.Models;
using Wabbajack.Services.OSIntegrated;
using Wabbajack.UserIntervention;
using Wabbajack.Util;
namespace Wabbajack
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App
{
private readonly IServiceProvider _serviceProvider;
private readonly IHost _host;
public App()
{
_host = Host.CreateDefaultBuilder(Array.Empty<string>())
.ConfigureLogging(c =>
{
c.ClearProviders();
})
.ConfigureServices((host, services) =>
{
ConfigureServices(services);
})
.Build();
_serviceProvider = _host.Services;
}
private static IServiceCollection ConfigureServices(IServiceCollection services)
{
RxApp.MainThreadScheduler = new DispatcherScheduler(Dispatcher.CurrentDispatcher);
services.AddOSIntegrated();
services.AddSingleton<CefService>();
services.AddTransient<MainWindow>();
services.AddTransient<MainWindowVM>();
services.AddSingleton<SystemParametersConstructor>();
services.AddSingleton<LauncherUpdater>();
services.AddSingleton<ResourceMonitor>();
services.AddAllSingleton<ILoggerProvider, LoggerProvider>();
services.AddSingleton<MainSettings>();
services.AddTransient<CompilerVM>();
services.AddTransient<InstallerVM>();
services.AddTransient<ModeSelectionVM>();
services.AddTransient<ModListGalleryVM>();
services.AddTransient<CompilerVM>();
services.AddTransient<InstallerVM>();
services.AddTransient<SettingsVM>();
services.AddTransient<WebBrowserVM>();
// Login Handlers
services.AddTransient<VectorPlexusLoginHandler>();
services.AddTransient<NexusLoginHandler>();
services.AddTransient<LoversLabLoginHandler>();
// Login Managers
services.AddAllSingleton<INeedsLogin, LoversLabLoginManager>();
services.AddAllSingleton<INeedsLogin, NexusLoginManager>();
services.AddAllSingleton<INeedsLogin, VectorPlexusLoginManager>();
return services;
}
private void OnStartup(object sender, StartupEventArgs e)
{
RxApp.MainThreadScheduler.Schedule(0, (_, _) =>
{
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
mainWindow!.Show();
return Disposable.Empty;
});
}
private void OnExit(object sender, ExitEventArgs e)
{
using (_host)
{
_host.StopAsync();
}
base.OnExit(e);
}
}
}

View File

@ -0,0 +1,19 @@
using System;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
namespace Wabbajack;
public static class Consts
{
public static RelativePath MO2IniName = "ModOrganizer.ini".ToRelativePath();
public static string AppName = "Wabbajack";
public static Uri WabbajackBuildServerUri => new("https://build.wabbajack.org");
public static Version CurrentMinimumWabbajackVersion { get; set; } = Version.Parse("2.3.0.0");
public static bool UseNetworkWorkaroundMode { get; set; } = false;
public static AbsolutePath CefCacheLocation { get; } = KnownFolders.WabbajackAppLocal.Combine("Cef");
public static byte SettingsVersion = 0;
public static RelativePath NativeSettingsJson = "native_settings.json".ToRelativePath();
}

View File

@ -0,0 +1,80 @@
using System;
using System.Globalization;
using System.Windows.Data;
using ReactiveUI;
using Wabbajack.Common;
using Wabbajack.Paths;
namespace Wabbajack
{
public class AbsolutePathToStringConverter : IBindingTypeConverter, IValueConverter
{
public int GetAffinityForObjects(Type fromType, Type toType)
{
if (toType == typeof(object)) return 1;
if (toType == typeof(string)) return 1;
if (toType == typeof(AbsolutePath)) return 1;
if (toType == typeof(AbsolutePath?)) return 1;
return 0;
}
public bool TryConvert(object @from, Type toType, object conversionHint, out object result)
{
if (toType == typeof(AbsolutePath))
{
if (@from is string s)
{
try
{
result = (AbsolutePath)s;
return true;
}
catch
{
result = (AbsolutePath)"";
return false;
}
}
if (@from is AbsolutePath abs)
{
result = abs;
return true;
}
}
else if (toType == typeof(string))
{
if (@from is string s)
{
result = default;
return false;
}
if (@from is AbsolutePath abs)
{
result = abs.ToString();
return true;
}
}
result = default;
return false;
}
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType != typeof(string))
throw new InvalidOperationException($"The target must be of type string");
if (value is AbsolutePath path)
{
return path.ToString();
}
return string.Empty;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return (AbsolutePath)(value as string);
}
}
}

View File

@ -0,0 +1,32 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace Wabbajack
{
[ValueConversion(typeof(Visibility), typeof(bool))]
public class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType != typeof(Visibility))
throw new InvalidOperationException($"The target must be of type {nameof(Visibility)}");
bool compareTo = true;
if (parameter is bool p)
{
compareTo = p;
}
else if (parameter is string str && str.ToUpper().Equals("FALSE"))
{
compareTo = false;
}
return ((bool)value) == compareTo ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,32 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace Wabbajack
{
[ValueConversion(typeof(Visibility), typeof(bool))]
public class BoolToVisibilityHiddenConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType != typeof(Visibility))
throw new InvalidOperationException($"The target must be of type {nameof(Visibility)}");
bool compareTo = true;
if (parameter is bool p)
{
compareTo = p;
}
else if (parameter is string str && str.ToUpper().Equals("FALSE"))
{
compareTo = false;
}
return ((bool)value) == compareTo ? Visibility.Visible : Visibility.Hidden;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using ReactiveUI;
namespace Wabbajack
{
public class CommandConverter : IBindingTypeConverter
{
public int GetAffinityForObjects(Type fromType, Type toType)
{
if (toType != typeof(ICommand)) return 0;
if (fromType == typeof(ICommand)
|| fromType == typeof(IReactiveCommand))
{
return 1;
}
return 0;
}
public bool TryConvert(object from, Type toType, object conversionHint, out object result)
{
if (from == null)
{
result = default(ICommand);
return true;
}
result = from as ICommand;
return result != null;
}
}
}

View File

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ReactiveUI;
using Splat;
namespace Wabbajack
{
public static class ConverterRegistration
{
public static void Register()
{
Locator.CurrentMutable.RegisterConstant(
new CommandConverter(),
typeof(IBindingTypeConverter)
);
Locator.CurrentMutable.RegisterConstant(
new IntDownCastConverter(),
typeof(IBindingTypeConverter)
);
Locator.CurrentMutable.RegisterConstant(
new PercentToDoubleConverter(),
typeof(IBindingTypeConverter)
);
Locator.CurrentMutable.RegisterConstant(
new AbsolutePathToStringConverter(),
typeof(IBindingTypeConverter));
}
}
}

View File

@ -0,0 +1,19 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace Wabbajack
{
public class EqualsToBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return Equals(value, parameter);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return parameter;
}
}
}

View File

@ -0,0 +1,20 @@
using System;
using System.Globalization;
using System.Windows.Data;
using Wabbajack.Common;
namespace Wabbajack
{
public class FileSizeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return ((long)value).ToFileSizeString();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using ReactiveUI;
namespace Wabbajack
{
public class IntDownCastConverter : IBindingTypeConverter
{
public int GetAffinityForObjects(Type fromType, Type toType)
{
if (toType == typeof(int) || fromType == typeof(int?)) return 1;
if (fromType == typeof(ICommand)
|| fromType == typeof(IReactiveCommand))
{
return 1;
}
return 0;
}
public bool TryConvert(object from, Type toType, object conversionHint, out object result)
{
if (from == null)
{
result = default(ICommand);
return true;
}
result = from as ICommand;
return result != null;
}
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace Wabbajack
{
[ValueConversion(typeof(bool), typeof(bool))]
public class InverseBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType != typeof(bool))
throw new InvalidOperationException($"The target must be of type bool");
return !((bool)value);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType != typeof(bool))
throw new InvalidOperationException($"The target must be of type bool");
return !((bool)value);
}
}
}

View File

@ -0,0 +1,37 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace Wabbajack
{
/// <summary>
/// Evaluates any object and converts it to a visibility based on if it is null.
/// By default it will show if the object is not null, and collapse when it is null.
/// If ConverterParameter is set to false, then this behavior is inverted
/// </summary>
public class IsNotNullVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType != typeof(Visibility))
throw new InvalidOperationException($"The target must be of type {nameof(Visibility)}");
bool compareTo = true;
if (parameter is bool p)
{
compareTo = p;
}
else if (parameter is string str && str.ToUpper().Equals("FALSE"))
{
compareTo = false;
}
bool isNull = value != null;
return isNull == compareTo ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
namespace Wabbajack
{
public class IsTypeVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType != typeof(Visibility))
throw new InvalidOperationException($"The target must be of type {nameof(Visibility)}");
if (!(parameter is Type paramType))
{
throw new ArgumentException();
}
if (value == null) return Visibility.Collapsed;
return paramType.Equals(value.GetType()) ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,27 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace DarkBlendTheme
{
public class LeftMarginMultiplierConverter : IValueConverter
{
public double Length { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var item = value as TreeViewItem;
if (item == null)
return new Thickness(0);
return new Thickness(Length * item.GetDepth(), 0, 0, 0);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using ReactiveUI;
using Wabbajack.Common;
using Wabbajack.RateLimiter;
namespace Wabbajack
{
public class PercentToDoubleConverter : IBindingTypeConverter
{
public int GetAffinityForObjects(Type fromType, Type toType)
{
if (toType == typeof(double)) return 1;
if (toType == typeof(double?)) return 1;
if (toType == typeof(Percent)) return 1;
if (toType == typeof(Percent?)) return 1;
return 0;
}
public bool TryConvert(object from, Type toType, object conversionHint, out object result)
{
if (toType == typeof(double))
{
if (from is Percent p)
{
result = p.Value;
return true;
}
result = 0d;
return false;
}
if (toType == typeof(double?))
{
if (from is Percent p)
{
result = p.Value;
return true;
}
if (from == null)
{
result = default(double?);
return true;
}
result = default(double?);
return false;
}
if (toType == typeof(Percent))
{
if (from is double d)
{
result = Percent.FactoryPutInRange(d);
return true;
}
result = Percent.Zero;
return false;
}
if (toType == typeof(Percent?))
{
if (from is double d)
{
result = Percent.FactoryPutInRange(d);
return true;
}
if (from == null)
{
result = default(Percent?);
return true;
}
result = Percent.Zero;
return false;
}
result = null;
return false;
}
}
}

View File

@ -0,0 +1,106 @@
using System;
namespace Wabbajack
{
public struct ErrorResponse : IErrorResponse
{
public static readonly ErrorResponse Success = Succeed();
public static readonly ErrorResponse Failure = new();
public bool Succeeded { get; }
public Exception? Exception { get; }
private readonly string _reason;
public bool Failed => !Succeeded;
public string Reason
{
get
{
if (Exception != null)
{
if (string.IsNullOrWhiteSpace(_reason))
{
return Exception.ToString();
}
else
{
return $"{_reason}: {Exception.Message}";
}
}
return _reason;
}
}
bool IErrorResponse.Succeeded => Succeeded;
Exception? IErrorResponse.Exception => Exception;
private ErrorResponse(
bool succeeded,
string? reason = null,
Exception? ex = null)
{
Succeeded = succeeded;
_reason = reason ?? string.Empty;
Exception = ex;
}
public override string ToString()
{
return $"({(Succeeded ? "Success" : "Fail")}, {Reason})";
}
#region Factories
public static ErrorResponse Succeed()
{
return new ErrorResponse(true);
}
public static ErrorResponse Succeed(string reason)
{
return new ErrorResponse(true, reason);
}
public static ErrorResponse Fail(string reason, Exception? ex = null)
{
return new ErrorResponse(false, reason: reason, ex: ex);
}
public static ErrorResponse Fail(Exception ex)
{
return new ErrorResponse(false, ex: ex);
}
public static ErrorResponse Fail()
{
return new ErrorResponse(false);
}
public static ErrorResponse Create(bool successful, string? reason = null)
{
return new ErrorResponse(successful, reason);
}
#endregion
public static ErrorResponse Convert(IErrorResponse err, bool nullIsSuccess = true)
{
if (err == null) return Create(nullIsSuccess);
return new ErrorResponse(err.Succeeded, err.Reason, err.Exception);
}
public static ErrorResponse FirstFail(params ErrorResponse[] responses)
{
foreach (var resp in responses)
{
if (resp.Failed) return resp;
}
return ErrorResponse.Success;
}
}
public interface IErrorResponse
{
bool Succeeded { get; }
Exception? Exception { get; }
string Reason { get; }
}
}

View File

@ -0,0 +1,135 @@
using System;
namespace Wabbajack
{
public struct GetResponse<T> : IEquatable<GetResponse<T>>, IErrorResponse
{
public static readonly GetResponse<T> Failure = new GetResponse<T>();
public readonly T Value;
public readonly bool Succeeded;
public readonly Exception? Exception;
private readonly string _reason;
public bool Failed => !Succeeded;
public string Reason
{
get
{
if (Exception != null)
{
return Exception.ToString();
}
return _reason;
}
}
bool IErrorResponse.Succeeded => Succeeded;
Exception? IErrorResponse.Exception => Exception;
private GetResponse(
bool succeeded,
T? val = default,
string? reason = null,
Exception? ex = null)
{
Value = val!;
Succeeded = succeeded;
_reason = reason ?? string.Empty;
Exception = ex;
}
public bool Equals(GetResponse<T> other)
{
return Succeeded == other.Succeeded
&& Equals(Value, other.Value);
}
public override bool Equals(object? obj)
{
if (!(obj is GetResponse<T> rhs)) return false;
return Equals(rhs);
}
public override int GetHashCode()
{
System.HashCode hash = new HashCode();
hash.Add(Value);
hash.Add(Succeeded);
return hash.ToHashCode();
}
public override string ToString()
{
return $"({(Succeeded ? "Success" : "Fail")}, {Value}, {Reason})";
}
public GetResponse<R> BubbleFailure<R>()
{
return new GetResponse<R>(
succeeded: false,
reason: _reason,
ex: Exception);
}
public GetResponse<R> Bubble<R>(Func<T, R> conv)
{
return new GetResponse<R>(
succeeded: Succeeded,
val: conv(Value),
reason: _reason,
ex: Exception);
}
public T EvaluateOrThrow()
{
if (Succeeded)
{
return Value;
}
throw new ArgumentException(Reason);
}
#region Factories
public static GetResponse<T> Succeed(T value)
{
return new GetResponse<T>(true, value);
}
public static GetResponse<T> Succeed(T value, string reason)
{
return new GetResponse<T>(true, value, reason);
}
public static GetResponse<T> Fail(string reason)
{
return new GetResponse<T>(false, reason: reason);
}
public static GetResponse<T> Fail(T val, string reason)
{
return new GetResponse<T>(false, val, reason);
}
public static GetResponse<T> Fail(Exception ex)
{
return new GetResponse<T>(false, ex: ex);
}
public static GetResponse<T> Fail(T val, Exception ex)
{
return new GetResponse<T>(false, val, ex: ex);
}
public static GetResponse<T> Fail(T val)
{
return new GetResponse<T>(false, val);
}
public static GetResponse<T> Create(bool successful, T? val = default(T), string? reason = null)
{
return new GetResponse<T>(successful, val!, reason);
}
#endregion
}
}

View File

@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using DynamicData;
namespace Wabbajack
{
public static class DynamicDataExt
{
public static IObservable<int> CollectionCount<TObject>(this IObservable<IChangeSet<TObject>> source)
{
int count = 0;
return source
.Select(changeSet =>
{
count += changeSet.Adds;
count -= changeSet.Removes;
return count;
})
.StartWith(0);
}
public static IObservable<int> CollectionCount<TObject, TKey>(this IObservable<IChangeSet<TObject, TKey>> source)
{
int count = 0;
return source
.Select(changeSet =>
{
count += changeSet.Adds;
count -= changeSet.Removes;
return count;
})
.StartWith(0);
}
public static IObservable<IChangeSet<TCache, TKey>> TransformAndCache<TObject, TKey, TCache>(
this IObservable<IChangeSet<TObject, TKey>> obs,
Func<TKey, TObject, TCache> onAdded,
Action<Change<TObject, TKey>, TCache> onUpdated)
{
var cache = new ChangeAwareCache<TCache, TKey>();
return obs
.Select(changeSet =>
{
foreach (var change in changeSet)
{
switch (change.Reason)
{
case ChangeReason.Add:
case ChangeReason.Update:
case ChangeReason.Refresh:
var lookup = cache.Lookup(change.Key);
TCache val;
if (lookup.HasValue)
{
val = lookup.Value;
}
else
{
val = onAdded(change.Key, change.Current);
cache.Add(val, change.Key);
}
onUpdated(change, val);
break;
case ChangeReason.Remove:
cache.Remove(change.Key);
break;
case ChangeReason.Moved:
break;
default:
throw new NotImplementedException();
}
}
return cache.CaptureChanges();
})
.Where(cs => cs.Count > 0);
}
}
}

View File

@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
using ReactiveUI;
namespace Wabbajack
{
public static class IViewForExt
{
public static IReactiveBinding<TView, TProp> OneWayBindStrict<TViewModel, TView, TProp>(
this TView view,
TViewModel viewModel,
Expression<Func<TViewModel, TProp>> vmProperty,
Expression<Func<TView, TProp>> viewProperty)
where TViewModel : class
where TView : class, IViewFor
{
return view.OneWayBind(
viewModel: viewModel,
vmProperty: vmProperty,
viewProperty: viewProperty);
}
public static IReactiveBinding<TView, TOut> OneWayBindStrict<TViewModel, TView, TProp, TOut>(
this TView view,
TViewModel viewModel,
Expression<Func<TViewModel, TProp>> vmProperty,
Expression<Func<TView, TOut>> viewProperty,
Func<TProp, TOut> selector)
where TViewModel : class
where TView : class, IViewFor
{
return view.OneWayBind(
viewModel: viewModel,
vmProperty: vmProperty,
viewProperty: viewProperty,
selector: selector);
}
public static IReactiveBinding<TView, (object view, bool isViewModel)> BindStrict<TViewModel, TView, TProp>(
this TView view,
TViewModel viewModel,
Expression<Func<TViewModel, TProp>> vmProperty,
Expression<Func<TView, TProp>> viewProperty)
where TViewModel : class
where TView : class, IViewFor
{
return view.Bind(
viewModel: viewModel,
vmProperty: vmProperty,
viewProperty: viewProperty);
}
public static IReactiveBinding<TView, (object view, bool isViewModel)> BindStrict<TViewModel, TView, TVMProp, TVProp, TDontCare>(
this TView view,
TViewModel viewModel,
Expression<Func<TViewModel, TVMProp>> vmProperty,
Expression<Func<TView, TVProp>> viewProperty,
IObservable<TDontCare> signalViewUpdate,
Func<TVMProp, TVProp> vmToViewConverter,
Func<TVProp, TVMProp> viewToVmConverter)
where TViewModel : class
where TView : class, IViewFor
{
return view.Bind(
viewModel: viewModel,
vmProperty: vmProperty,
viewProperty: viewProperty,
signalViewUpdate: signalViewUpdate,
vmToViewConverter: vmToViewConverter,
viewToVmConverter: viewToVmConverter);
}
public static IReactiveBinding<TView, (object view, bool isViewModel)> BindStrict<TViewModel, TView, TVMProp, TVProp>(
this TView view,
TViewModel viewModel,
Expression<Func<TViewModel, TVMProp>> vmProperty,
Expression<Func<TView, TVProp>> viewProperty,
Func<TVMProp, TVProp> vmToViewConverter,
Func<TVProp, TVMProp> viewToVmConverter)
where TViewModel : class
where TView : class, IViewFor
{
return view.Bind(
viewModel: viewModel,
vmProperty: vmProperty,
viewProperty: viewProperty,
vmToViewConverter: vmToViewConverter,
viewToVmConverter: viewToVmConverter);
}
public static IDisposable BindToStrict<TValue, TTarget>(
this IObservable<TValue> @this,
TTarget target,
Expression<Func<TTarget, TValue>> property)
where TTarget : class
{
return @this
.ObserveOnGuiThread()
.BindTo<TValue, TTarget, TValue>(target, property);
}
/// <summary>
/// Just a function to signify a field is being used, so it triggers compile errors if it changes
/// </summary>
public static void MarkAsNeeded<TView, TViewModel, TVMProp>(
this TView view,
TViewModel viewModel,
Expression<Func<TViewModel, TVMProp>> vmProperty)
where TViewModel : class
where TView : class, IViewFor
{
}
}
}

View File

@ -0,0 +1,160 @@
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Threading.Tasks;
using System.Windows.Threading;
using DynamicData;
using DynamicData.Kernel;
using ReactiveUI;
using Wabbajack;
using Wabbajack.Extensions;
namespace Wabbajack
{
public static class ReactiveUIExt
{
/// <summary>
/// Convenience function to not have to specify the selector function in the default ReactiveUI WhenAny() call.
/// Subscribes to changes in a property on a given object.
/// </summary>
/// <typeparam name="TSender">Type of object to watch</typeparam>
/// <typeparam name="TRet">The type of property watched</typeparam>
/// <param name="This">Object to watch</param>
/// <param name="property1">Expression path to the property to subscribe to</param>
/// <returns></returns>
public static IObservable<TRet?> WhenAny<TSender, TRet>(
this TSender This,
Expression<Func<TSender, TRet?>> property1)
where TSender : class
{
return This.WhenAny(property1, selector: x => x.GetValue());
}
/// <summary>
/// Convenience wrapper to observe following calls on the GUI thread.
/// </summary>
public static IObservable<T> ObserveOnGuiThread<T>(this IObservable<T> source)
{
return source.ObserveOn(RxApp.MainThreadScheduler);
}
/// <summary>
/// Like IObservable.Select but supports async map functions
/// </summary>
/// <param name="source"></param>
/// <param name="f"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static IObservable<TOut> SelectAsync<TIn, TOut>(this IObservable<TIn> source, Func<TIn, Task<TOut>> f)
{
return source.Select(itm => Observable.FromAsync(async () => await f(itm))).Merge(10);
}
public static IObservable<Unit> StartingExecution(this IReactiveCommand cmd)
{
return cmd.IsExecuting
.DistinctUntilChanged()
.Where(x => x)
.Unit();
}
public static IObservable<Unit> EndingExecution(this IReactiveCommand cmd)
{
return cmd.IsExecuting
.DistinctUntilChanged()
.Pairwise()
.Where(x => x.Previous && !x.Current)
.Unit();
}
/// These snippets were provided by RolandPheasant (author of DynamicData)
/// They'll be going into the official library at some point, but are here for now.
#region Dynamic Data EnsureUniqueChanges
/// <summary>
/// Removes outdated key events from a changeset, only leaving the last relevent change for each key.
/// </summary>
public static IObservable<IChangeSet<TObject, TKey>> EnsureUniqueChanges<TObject, TKey>(this IObservable<IChangeSet<TObject, TKey>> source)
where TKey : notnull
{
return source.Select(EnsureUniqueChanges);
}
/// <summary>
/// Removes outdated key events from a changeset, only leaving the last relevent change for each key.
/// </summary>
public static IChangeSet<TObject, TKey> EnsureUniqueChanges<TObject, TKey>(this IChangeSet<TObject, TKey> input)
where TKey : notnull
{
var changes = input
.GroupBy(kvp => kvp.Key)
.Select(g => g.Aggregate(Optional<Change<TObject, TKey>>.None, Reduce))
.Where(x => x.HasValue)
.Select(x => x.Value);
return new ChangeSet<TObject, TKey>(changes);
}
public static ObservableAsPropertyHelper<TRet> ToGuiProperty<TRet>(
this IObservable<TRet> source,
ViewModel vm,
string property,
TRet? initialValue = default,
bool deferSubscription = false)
{
return source
.ToProperty(vm, property, initialValue, deferSubscription, RxApp.MainThreadScheduler)
.DisposeWith(vm.CompositeDisposable)!;
}
/*
public static void ToGuiProperty<TRet>(
this IObservable<TRet> source,
ViewModel vm,
string property,
out ObservableAsPropertyHelper<TRet> result,
TRet initialValue = default,
bool deferSubscription = false)
{
source.ToProperty(vm, property, out result!, initialValue, deferSubscription, RxApp.MainThreadScheduler)
.DisposeWith(vm.CompositeDisposable);
}*/
internal static Optional<Change<TObject, TKey>> Reduce<TObject, TKey>(Optional<Change<TObject, TKey>> previous, Change<TObject, TKey> next)
where TKey : notnull
{
if (!previous.HasValue)
{
return next;
}
var previousValue = previous.Value;
switch (previousValue.Reason)
{
case ChangeReason.Add when next.Reason == ChangeReason.Remove:
return Optional<Change<TObject, TKey>>.None;
case ChangeReason.Remove when next.Reason == ChangeReason.Add:
return new Change<TObject, TKey>(ChangeReason.Update, next.Key, next.Current, previousValue.Current, next.CurrentIndex, previousValue.CurrentIndex);
case ChangeReason.Add when next.Reason == ChangeReason.Update:
return new Change<TObject, TKey>(ChangeReason.Add, next.Key, next.Current, next.CurrentIndex);
case ChangeReason.Update when next.Reason == ChangeReason.Update:
return new Change<TObject, TKey>(ChangeReason.Update, previousValue.Key, next.Current, previousValue.Previous, next.CurrentIndex, previousValue.PreviousIndex);
default:
return next;
}
}
#endregion
}
}

View File

@ -0,0 +1,234 @@
using System;
using System.Reactive;
using System.Reactive.Concurrency;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading.Tasks;
namespace Wabbajack.Extensions
{
public static class RxExt
{
/// <summary>
/// Convenience function that discards events that are null
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="source"></param>
/// <returns>Source events that are not null</returns>
public static IObservable<T> NotNull<T>(this IObservable<T?> source)
where T : class
{
return source
.Where(u => u != null)
.Select(u => u!);
}
/// <summary>
/// Converts any observable to type Unit. Useful for when you care that a signal occurred,
/// but don't care about what its value is downstream.
/// </summary>
/// <returns>An observable that returns Unit anytime the source signal fires an event.</returns>
public static IObservable<Unit> Unit<T>(this IObservable<T> source)
{
return source.Select(_ => System.Reactive.Unit.Default);
}
/// <summary>
/// Convenience operator to subscribe to the source observable, only when a second "switch" observable is on.
/// When the switch is on, the source will be subscribed to, and its updates passed through.
/// When the switch is off, the subscription to the source observable will be stopped, and no signal will be published.
/// </summary>
/// <param name="source">Source observable to subscribe to if on</param>
/// <param name="filterSwitch">On/Off signal of whether to subscribe to source observable</param>
/// <returns>Observable that publishes data from source, if the switch is on.</returns>
public static IObservable<T> FlowSwitch<T>(this IObservable<T> source, IObservable<bool> filterSwitch)
{
return filterSwitch
.DistinctUntilChanged()
.Select(on =>
{
if (on)
{
return source;
}
else
{
return Observable.Empty<T>();
}
})
.Switch();
}
/// <summary>
/// Convenience operator to subscribe to the source observable, only when a second "switch" observable is on.
/// When the switch is on, the source will be subscribed to, and its updates passed through.
/// When the switch is off, the subscription to the source observable will be stopped, and no signal will be published.
/// </summary>
public static IObservable<T> FlowSwitch<T>(this IObservable<T> source, IObservable<bool> filterSwitch, T valueWhenOff)
{
return filterSwitch
.DistinctUntilChanged()
.Select(on =>
{
if (on)
{
return source;
}
else
{
return Observable.Return<T>(valueWhenOff);
}
})
.Switch();
}
/// Inspiration:
/// http://reactivex.io/documentation/operators/debounce.html
/// https://stackoverflow.com/questions/20034476/how-can-i-use-reactive-extensions-to-throttle-events-using-a-max-window-size
public static IObservable<T> Debounce<T>(this IObservable<T> source, TimeSpan interval, IScheduler? scheduler = null)
{
scheduler ??= Scheduler.Default;
return Observable.Create<T>(o =>
{
var hasValue = false;
bool throttling = false;
T? value = default;
var dueTimeDisposable = new SerialDisposable();
void internalCallback()
{
if (hasValue)
{
// We have another value that came in to fire.
// Reregister for callback
dueTimeDisposable.Disposable = scheduler!.Schedule(interval, internalCallback);
o.OnNext(value!);
value = default;
hasValue = false;
}
else
{
// Nothing to do, throttle is complete.
throttling = false;
}
}
return source.Subscribe(
onNext: (x) =>
{
if (!throttling)
{
// Fire initial value
o.OnNext(x);
// Mark that we're throttling
throttling = true;
// Register for callback when throttle is complete
dueTimeDisposable.Disposable = scheduler.Schedule(interval, internalCallback);
}
else
{
// In the middle of throttle
// Save value and return
hasValue = true;
value = x;
}
},
onError: o.OnError,
onCompleted: o.OnCompleted);
});
}
public static IObservable<Unit> SelectTask<T>(this IObservable<T> source, Func<T, Task> task)
{
return source
.SelectMany(async i =>
{
await task(i).ConfigureAwait(false);
return System.Reactive.Unit.Default;
});
}
public static IObservable<Unit> SelectTask<T>(this IObservable<T> source, Func<Task> task)
{
return source
.SelectMany(async _ =>
{
await task().ConfigureAwait(false);
return System.Reactive.Unit.Default;
});
}
public static IObservable<R> SelectTask<T, R>(this IObservable<T> source, Func<Task<R>> task)
{
return source
.SelectMany(_ => task());
}
public static IObservable<R> SelectTask<T, R>(this IObservable<T> source, Func<T, Task<R>> task)
{
return source
.SelectMany(x => task(x));
}
public static IObservable<T> DoTask<T>(this IObservable<T> source, Func<T, Task> task)
{
return source
.SelectMany(async (x) =>
{
await task(x).ConfigureAwait(false);
return x;
});
}
public static IObservable<R> WhereCastable<T, R>(this IObservable<T> source)
where R : class
where T : class
{
return source
.Select(x => x as R)
.NotNull();
}
public static IObservable<bool> Invert(this IObservable<bool> source)
{
return source.Select(x => !x);
}
public static IObservable<(T Previous, T Current)> Pairwise<T>(this IObservable<T> source)
{
T? prevStorage = default;
return source.Select(i =>
{
var prev = prevStorage;
prevStorage = i;
return (prev, i);
})!;
}
public static IObservable<T> DelayInitial<T>(this IObservable<T> source, TimeSpan delay, IScheduler scheduler)
{
return source.FlowSwitch(
Observable.Return(System.Reactive.Unit.Default)
.Delay(delay, scheduler)
.Select(_ => true)
.StartWith(false));
}
public static IObservable<T?> DisposeOld<T>(this IObservable<T?> source)
where T : class, IDisposable
{
return source
.StartWith(default(T))
.Pairwise()
.Do(x =>
{
if (x.Previous != null)
{
x.Previous.Dispose();
}
})
.Select(x => x.Current);
}
}
}

View File

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ReactiveUI />
</Weavers>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="ReactiveUI" minOccurs="0" maxOccurs="1" type="xs:anyType" />
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

View File

@ -0,0 +1,12 @@
using System;
namespace Wabbajack.Interventions
{
public abstract class AErrorMessage : Exception, IException
{
public DateTime Timestamp { get; } = DateTime.Now;
public abstract string ShortDescription { get; }
public abstract string ExtendedDescription { get; }
Exception IException.Exception => this;
}
}

View File

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using ReactiveUI;
using Wabbajack.Common;
using Wabbajack.DTOs.Interventions;
using Wabbajack.Interventions;
namespace Wabbajack
{
public abstract class AUserIntervention : ReactiveObject, IUserIntervention
{
public DateTime Timestamp { get; } = DateTime.Now;
public abstract string ShortDescription { get; }
public abstract string ExtendedDescription { get; }
private bool _handled;
public bool Handled { get => _handled; set => this.RaiseAndSetIfChanged(ref _handled, value); }
public CancellationToken Token { get; }
public void SetException(Exception exception)
{
throw new NotImplementedException();
}
public abstract void Cancel();
public ICommand CancelCommand { get; }
public AUserIntervention()
{
CancelCommand = ReactiveCommand.Create(() => Cancel());
}
}
}

View File

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using ReactiveUI;
namespace Wabbajack
{
public abstract class ConfirmationIntervention : AUserIntervention
{
public enum Choice
{
Continue,
Abort
}
private TaskCompletionSource<Choice> _source = new TaskCompletionSource<Choice>();
public Task<Choice> Task => _source.Task;
public ICommand ConfirmCommand { get; }
public ConfirmationIntervention()
{
ConfirmCommand = ReactiveCommand.Create(() => Confirm());
}
public override void Cancel()
{
Handled = true;
_source.SetResult(Choice.Abort);
}
public void Confirm()
{
Handled = true;
_source.SetResult(Choice.Continue);
}
}
}

View File

@ -0,0 +1,6 @@
namespace Wabbajack.Interventions
{
public interface IError : IStatusMessage
{
}
}

View File

@ -0,0 +1,9 @@
using System;
namespace Wabbajack.Interventions
{
public interface IException : IError
{
Exception Exception { get; }
}
}

View File

@ -0,0 +1,11 @@
using System;
namespace Wabbajack.Interventions
{
public interface IStatusMessage
{
DateTime Timestamp { get; }
string ShortDescription { get; }
string ExtendedDescription { get; }
}
}

View File

@ -0,0 +1,172 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.VisualBasic.CompilerServices;
using Newtonsoft.Json;
using Wabbajack.Downloaders;
using Wabbajack.DTOs;
using Wabbajack.DTOs.DownloadStates;
using Wabbajack.DTOs.JsonConverters;
using Wabbajack.Networking.Http;
using Wabbajack.Networking.Http.Interfaces;
using Wabbajack.Networking.WabbajackClientApi;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
using Wabbajack.RateLimiter;
namespace Wabbajack
{
public class LauncherUpdater
{
private readonly ILogger<LauncherUpdater> _logger;
private readonly HttpClient _client;
private readonly Client _wjclient;
private readonly DTOSerializer _dtos;
private readonly DownloadDispatcher _downloader;
private static Uri GITHUB_REPO_RELEASES = new("https://api.github.com/repos/wabbajack-tools/wabbajack/releases");
public LauncherUpdater(ILogger<LauncherUpdater> logger, HttpClient client, Client wjclient, DTOSerializer dtos,
DownloadDispatcher downloader)
{
_logger = logger;
_client = client;
_wjclient = wjclient;
_dtos = dtos;
_downloader = downloader;
}
public static Lazy<AbsolutePath> CommonFolder = new (() =>
{
var entryPoint = KnownFolders.EntryPoint;
// If we're not in a folder that looks like a version, abort
if (!Version.TryParse(entryPoint.FileName.ToString(), out var version))
{
return entryPoint;
}
// If we're not in a folder that has Wabbajack.exe in the parent folder, abort
if (!entryPoint.Parent.Combine(Consts.AppName).WithExtension(new Extension(".exe")).FileExists())
{
return entryPoint;
}
return entryPoint.Parent;
});
public async Task Run()
{
if (CommonFolder.Value == KnownFolders.EntryPoint)
{
_logger.LogInformation("Outside of standard install folder, not updating");
return;
}
var version = Version.Parse(KnownFolders.EntryPoint.FileName.ToString());
var oldVersions = CommonFolder.Value
.EnumerateDirectories()
.Select(f => Version.TryParse(f.FileName.ToString(), out var ver) ? (ver, f) : default)
.Where(f => f != default)
.Where(f => f.ver < version)
.Select(f => f!)
.OrderByDescending(f => f)
.Skip(2)
.ToArray();
foreach (var (_, path) in oldVersions)
{
_logger.LogInformation("Deleting old Wabbajack version at: {Path}", path);
path.DeleteDirectory();
}
var release = (await GetReleases())
.Select(release => Version.TryParse(release.Tag, out version) ? (version, release) : default)
.Where(r => r != default)
.OrderByDescending(r => r.version)
.Select(r =>
{
var (version, release) = r;
var asset = release.Assets.FirstOrDefault(a => a.Name == "Wabbajack.exe");
return asset != default ? (version, release, asset) : default;
})
.FirstOrDefault();
var launcherFolder = KnownFolders.EntryPoint.Parent;
var exePath = launcherFolder.Combine("Wabbajack.exe");
var launcherVersion = FileVersionInfo.GetVersionInfo(exePath.ToString());
if (release != default && release.version > Version.Parse(launcherVersion.FileVersion!))
{
_logger.LogInformation("Updating Launcher from {OldVersion} to {NewVersion}", launcherVersion.FileVersion, release.version);
var tempPath = launcherFolder.Combine("Wabbajack.exe.temp");
await _downloader.Download(new Archive
{
State = new Http {Url = release.asset.BrowserDownloadUrl!},
Name = release.asset.Name,
Size = release.asset.Size
}, tempPath, CancellationToken.None);
if (tempPath.Size() != release.asset.Size)
{
_logger.LogInformation(
"Downloaded launcher did not match expected size: {DownloadedSize} expected {ExpectedSize}", tempPath.Size(), release.asset.Size);
return;
}
if (exePath.FileExists())
exePath.Delete();
await tempPath.MoveToAsync(exePath, true, CancellationToken.None);
_logger.LogInformation("Finished updating wabbajack");
await _wjclient.SendMetric("updated_launcher", $"{launcherVersion.FileVersion} -> {release.version}");
}
}
private async Task<Release[]> GetReleases()
{
_logger.LogInformation("Getting new Wabbajack version list");
var msg = MakeMessage(GITHUB_REPO_RELEASES);
return await _client.GetJsonFromSendAsync<Release[]>(msg, _dtos.Options);
}
private HttpRequestMessage MakeMessage(Uri uri)
{
var msg = new HttpRequestMessage(HttpMethod.Get, uri);
msg.UseChromeUserAgent();
return msg;
}
class Release
{
[JsonProperty("tag_name")] public string Tag { get; set; } = "";
[JsonProperty("assets")] public Asset[] Assets { get; set; } = Array.Empty<Asset>();
}
class Asset
{
[JsonProperty("browser_download_url")]
public Uri? BrowserDownloadUrl { get; set; }
[JsonProperty("name")] public string Name { get; set; } = "";
[JsonProperty("size")] public long Size { get; set; } = 0;
}
}
}

View File

@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using Alphaleonis.Win32.Filesystem;
using CefSharp;
using CefSharp.OffScreen;
using Wabbajack.Common;
using Wabbajack.DTOs.JsonConverters;
using Cookie = CefSharp.Cookie;
namespace Wabbajack.LibCefHelpers
{
public static class Helpers
{
public static HttpRequestMessage MakeMessage(HttpMethod method, Uri uri, IEnumerable<Cookie> cookies, string referer)
{
var msg = new HttpRequestMessage(method, uri);
msg.Headers.Add("Referrer", referer);
var cs = string.Join(",", cookies.Select(c => $"{c.Name}={c.Value}"));
msg.Headers.Add("Cookie", cs);
return msg;
}
private static CookieContainer ToCookieContainer(IEnumerable<Cookie> cookies)
{
var container = new CookieContainer();
cookies
.Do(cookie =>
{
container.Add(new System.Net.Cookie(cookie.Name, cookie.Value, cookie.Path, cookie.Domain));
});
return container;
}
public static async Task<DTOs.Logins.Cookie[]> GetCookies(string domainEnding = "")
{
var manager = Cef.GetGlobalCookieManager();
var visitor = new CookieVisitor();
if (!manager.VisitAllCookies(visitor))
return Array.Empty<DTOs.Logins.Cookie>();
var cc = await visitor.Task;
return (await visitor.Task).Where(c => c.Domain.EndsWith(domainEnding)).ToArray();
}
private class CookieVisitor : ICookieVisitor
{
TaskCompletionSource<List<DTOs.Logins.Cookie>> _source = new();
public Task<List<DTOs.Logins.Cookie>> Task => _source.Task;
public List<DTOs.Logins.Cookie> Cookies { get; } = new ();
public void Dispose()
{
_source.SetResult(Cookies);
}
public bool Visit(CefSharp.Cookie cookie, int count, int total, ref bool deleteCookie)
{
Cookies.Add(new DTOs.Logins.Cookie
{
Name = cookie.Name,
Value = cookie.Value,
Domain = cookie.Domain,
Path = cookie.Path
});
if (count == total)
_source.SetResult(Cookies);
deleteCookie = false;
return true;
}
}
public static void ClearCookies()
{
var manager = Cef.GetGlobalCookieManager();
var visitor = new CookieDeleter();
manager.VisitAllCookies(visitor);
}
public static async Task DeleteCookiesWhere(Func<DTOs.Logins.Cookie,bool> filter)
{
var manager = Cef.GetGlobalCookieManager();
var visitor = new CookieDeleter(filter);
manager.VisitAllCookies(visitor);
}
}
class CookieDeleter : ICookieVisitor
{
private Func<DTOs.Logins.Cookie, bool>? _filter;
public CookieDeleter(Func<DTOs.Logins.Cookie, bool>? filter = null)
{
_filter = filter;
}
public void Dispose()
{
}
public bool Visit(Cookie cookie, int count, int total, ref bool deleteCookie)
{
if (_filter == null)
{
deleteCookie = true;
}
else
{
var conv = new DTOs.Logins.Cookie
{
Name = cookie.Name, Domain = cookie.Domain, Value = cookie.Value, Path = cookie.Path
};
if (_filter(conv))
deleteCookie = true;
}
return true;
}
}
}

View File

@ -0,0 +1,87 @@
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
namespace Wabbajack.Models;
public class LoadingLock : ReactiveObject, IDisposable
{
private readonly CompositeDisposable _disposable;
[Reactive]
public ErrorResponse? ErrorState { get; set; }
public LoadingLock()
{
_disposable = new CompositeDisposable();
this.WhenAnyValue(vm => vm.LoadLevel)
.StartWith(0)
.Subscribe(v => IsLoading = v > 0)
.DisposeWith(_disposable);
this.WhenAnyValue(vm => vm.LoadLevel)
.StartWith(0)
.Subscribe(v => IsNotLoading = v == 0)
.DisposeWith(_disposable);
}
[Reactive] public int LoadLevel { get; private set; }
[Reactive] public bool IsLoading { get; private set; }
[Reactive] public bool IsNotLoading { get; private set; }
public IObservable<bool> IsLoadingObservable => this.WhenAnyValue(ll => ll.IsLoading);
public IObservable<bool> IsNotLoadingObservable => this.WhenAnyValue(ll => ll.IsNotLoading);
public void Dispose()
{
GC.SuppressFinalize(this);
_disposable.Dispose();
}
public LockContext WithLoading()
{
RxApp.MainThreadScheduler.Schedule(0, (_, _) => { LoadLevel++;
return Disposable.Empty;
});
return new LockContext(this);
}
public class LockContext : IDisposable
{
private readonly LoadingLock _parent;
private bool _disposed;
public LockContext(LoadingLock parent)
{
_parent = parent;
_disposed = false;
}
public void Succeed()
{
_parent.ErrorState = ErrorResponse.Success;
Dispose();
}
public void Fail()
{
_parent.ErrorState = ErrorResponse.Failure;
Dispose();
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
RxApp.MainThreadScheduler.Schedule(0, (_, _) => { _parent.LoadLevel--;
return Disposable.Empty;
});
}
}
}

View File

@ -0,0 +1,14 @@
using System.Windows.Input;
using System.Windows.Media;
using ReactiveUI;
namespace Wabbajack.LoginManagers;
public interface INeedsLogin
{
string SiteName { get; }
ICommand TriggerLogin { get; set; }
ICommand ClearLogin { get; set; }
ImageSource Icon { get; set; }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 B

View File

@ -0,0 +1,64 @@
using System;
using System.Drawing;
using System.Reactive.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Microsoft.Extensions.Logging;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.Common;
using Wabbajack.DTOs.Interventions;
using Wabbajack.DTOs.Logins;
using Wabbajack.Messages;
using Wabbajack.Networking.Http.Interfaces;
namespace Wabbajack.LoginManagers;
public class LoversLabLoginManager : ViewModel, INeedsLogin
{
private readonly ILogger<LoversLabLoginManager> _logger;
private readonly ITokenProvider<LoversLabLoginState> _token;
private readonly IUserInterventionHandler _handler;
public string SiteName { get; } = "Lovers Lab";
public ICommand TriggerLogin { get; set; }
public ICommand ClearLogin { get; set; }
public ImageSource Icon { get; set; }
[Reactive]
public bool HaveLogin { get; set; }
public LoversLabLoginManager(ILogger<LoversLabLoginManager> logger, ITokenProvider<LoversLabLoginState> token)
{
_logger = logger;
_token = token;
RefreshTokenState();
ClearLogin = ReactiveCommand.CreateFromTask(async () =>
{
_logger.LogInformation("Deleting Login information for {SiteName}", SiteName);
await _token.Delete();
RefreshTokenState();
}, this.WhenAnyValue(v => v.HaveLogin));
Icon = BitmapFrame.Create(
typeof(LoversLabLoginManager).Assembly.GetManifestResourceStream("Wabbajack.App.Wpf.LoginManagers.Icons.lovers_lab.png")!);
TriggerLogin = ReactiveCommand.CreateFromTask(async () =>
{
_logger.LogInformation("Logging into {SiteName}", SiteName);
await LoversLabLogin.Send();
RefreshTokenState();
}, this.WhenAnyValue(v => v.HaveLogin).Select(v => !v));
}
private void RefreshTokenState()
{
HaveLogin = _token.HaveToken();
}
}

View File

@ -0,0 +1,58 @@
using System.Reactive.Linq;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Microsoft.Extensions.Logging;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.DTOs.Interventions;
using Wabbajack.DTOs.Logins;
using Wabbajack.Messages;
using Wabbajack.Networking.Http.Interfaces;
namespace Wabbajack.LoginManagers;
public class NexusLoginManager : ViewModel, INeedsLogin
{
private readonly ILogger<NexusLoginManager> _logger;
private readonly ITokenProvider<NexusApiState> _token;
private readonly IUserInterventionHandler _handler;
public string SiteName { get; } = "Nexus Mods";
public ICommand TriggerLogin { get; set; }
public ICommand ClearLogin { get; set; }
public ImageSource Icon { get; set; }
[Reactive]
public bool HaveLogin { get; set; }
public NexusLoginManager(ILogger<NexusLoginManager> logger, ITokenProvider<NexusApiState> token)
{
_logger = logger;
_token = token;
RefreshTokenState();
ClearLogin = ReactiveCommand.CreateFromTask(async () =>
{
_logger.LogInformation("Deleting Login information for {SiteName}", SiteName);
await _token.Delete();
RefreshTokenState();
}, this.WhenAnyValue(v => v.HaveLogin));
Icon = BitmapFrame.Create(
typeof(NexusLoginManager).Assembly.GetManifestResourceStream("Wabbajack.App.Wpf.LoginManagers.Icons.nexus.png")!);
TriggerLogin = ReactiveCommand.CreateFromTask(async () =>
{
_logger.LogInformation("Logging into {SiteName}", SiteName);
await NexusLogin.Send();
RefreshTokenState();
}, this.WhenAnyValue(v => v.HaveLogin).Select(v => !v));
}
private void RefreshTokenState()
{
HaveLogin = _token.HaveToken();
}
}

View File

@ -0,0 +1,64 @@
using System;
using System.Drawing;
using System.Reactive.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Microsoft.Extensions.Logging;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.Common;
using Wabbajack.DTOs.Interventions;
using Wabbajack.DTOs.Logins;
using Wabbajack.Messages;
using Wabbajack.Networking.Http.Interfaces;
namespace Wabbajack.LoginManagers;
public class VectorPlexusLoginManager : ViewModel, INeedsLogin
{
private readonly ILogger<VectorPlexusLoginManager> _logger;
private readonly ITokenProvider<VectorPlexusLoginState> _token;
private readonly IUserInterventionHandler _handler;
public string SiteName { get; } = "Vector Plexus";
public ICommand TriggerLogin { get; set; }
public ICommand ClearLogin { get; set; }
public ImageSource Icon { get; set; }
[Reactive]
public bool HaveLogin { get; set; }
public VectorPlexusLoginManager(ILogger<VectorPlexusLoginManager> logger, ITokenProvider<VectorPlexusLoginState> token)
{
_logger = logger;
_token = token;
RefreshTokenState();
ClearLogin = ReactiveCommand.CreateFromTask(async () =>
{
_logger.LogInformation("Deleting Login information for {SiteName}", SiteName);
await _token.Delete();
RefreshTokenState();
}, this.WhenAnyValue(v => v.HaveLogin));
Icon = BitmapFrame.Create(
typeof(VectorPlexusLoginManager).Assembly.GetManifestResourceStream("Wabbajack.App.Wpf.LoginManagers.Icons.vector_plexus.png")!);
TriggerLogin = ReactiveCommand.CreateFromTask(async () =>
{
_logger.LogInformation("Logging into {SiteName}", SiteName);
await VectorPlexusLogin.Send();
RefreshTokenState();
}, this.WhenAnyValue(v => v.HaveLogin).Select(v => !v));
}
private void RefreshTokenState()
{
HaveLogin = _token.HaveToken();
}
}

View File

@ -0,0 +1,34 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using ReactiveUI;
using Wabbajack.DTOs.Interventions;
namespace Wabbajack.Messages;
public class ALoginMessage : IUserIntervention
{
private readonly CancellationTokenSource _source;
public TaskCompletionSource CompletionSource { get; }
public CancellationToken Token => _source.Token;
public void SetException(Exception exception)
{
CompletionSource.SetException(exception);
_source.Cancel();
}
public ALoginMessage()
{
CompletionSource = new TaskCompletionSource();
_source = new CancellationTokenSource();
}
public void Cancel()
{
_source.Cancel();
CompletionSource.TrySetCanceled();
}
public bool Handled => CompletionSource.Task.IsCompleted;
}

View File

@ -0,0 +1,12 @@
using ReactiveUI;
namespace Wabbajack.Messages;
public class LoadLastLoadedModlist
{
public static void Send()
{
MessageBus.Current.SendMessage(new LoadLastLoadedModlist());
}
}

View File

@ -0,0 +1,23 @@
using ReactiveUI;
using Wabbajack.DTOs;
using Wabbajack.Paths;
namespace Wabbajack.Messages;
public class LoadModlistForInstalling
{
public AbsolutePath Path { get; }
public ModlistMetadata Metadata { get; }
public LoadModlistForInstalling(AbsolutePath path, ModlistMetadata metadata)
{
Path = path;
Metadata = metadata;
}
public static void Send(AbsolutePath path, ModlistMetadata metadata)
{
MessageBus.Current.SendMessage(new LoadModlistForInstalling(path, metadata));
}
}

View File

@ -0,0 +1,15 @@
using System.Threading.Tasks;
using ReactiveUI;
namespace Wabbajack.Messages;
public class LoversLabLogin : ALoginMessage
{
public static Task Send()
{
var msg = new LoversLabLogin();
MessageBus.Current.SendMessage(msg);
return msg.CompletionSource.Task;
}
}

View File

@ -0,0 +1,11 @@
using ReactiveUI;
namespace Wabbajack.Messages;
public class NavigateBack
{
public static void Send()
{
MessageBus.Current.SendMessage(new NavigateBack());
}
}

View File

@ -0,0 +1,21 @@
using ReactiveUI;
using Wabbajack;
namespace Wabbajack.Messages;
public class NavigateTo
{
public ViewModel ViewModel { get; }
private NavigateTo(ViewModel vm)
{
ViewModel = vm;
}
public static void Send<T>(T vm)
where T : ViewModel
{
MessageBus.Current.SendMessage(new NavigateTo(vm));
}
}

View File

@ -0,0 +1,30 @@
using ReactiveUI;
namespace Wabbajack.Messages;
public class NavigateToGlobal
{
public enum ScreenType
{
ModeSelectionView,
ModListGallery,
Installer,
Settings,
Compiler,
ModListContents,
WebBrowser
}
public ScreenType Screen { get; }
private NavigateToGlobal(ScreenType screen)
{
Screen = screen;
}
public static void Send(ScreenType screen)
{
MessageBus.Current.SendMessage(new NavigateToGlobal(screen));
}
}

View File

@ -0,0 +1,22 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using ReactiveUI;
using Wabbajack.DTOs.Interventions;
using Wabbajack.Networking.Http.Interfaces;
namespace Wabbajack.Messages;
public class NexusLogin : ALoginMessage
{
public NexusLogin()
{
}
public static Task Send()
{
var msg = new NexusLogin();
MessageBus.Current.SendMessage(msg);
return msg.CompletionSource.Task;
}
}

View File

@ -0,0 +1,22 @@
using System.Windows.Shell;
using ReactiveUI;
namespace Wabbajack.Messages;
public class TaskBarUpdate
{
public string Description { get; init; }
public double ProgressValue { get; init; }
public TaskbarItemProgressState State { get; init; }
public static void Send(string description, TaskbarItemProgressState state = TaskbarItemProgressState.None,
double progressValue = 0)
{
MessageBus.Current.SendMessage(new TaskBarUpdate()
{
Description = description,
ProgressValue = progressValue,
State = state
});
}
}

View File

@ -0,0 +1,15 @@
using System.Threading.Tasks;
using ReactiveUI;
namespace Wabbajack.Messages;
public class VectorPlexusLogin : ALoginMessage
{
public static Task Send()
{
var msg = new VectorPlexusLogin();
MessageBus.Current.SendMessage(msg);
return msg.CompletionSource.Task;
}
}

View File

@ -0,0 +1,72 @@
using System;
using System.Reactive.Subjects;
using CefSharp;
using CefSharp.Wpf;
using Microsoft.Extensions.Logging;
namespace Wabbajack.Models;
public class CefService
{
private readonly ILogger<CefService> _logger;
private bool Inited { get; set; } = false;
private readonly Subject<string> _schemeStream = new();
public IObservable<string> SchemeStream => _schemeStream;
public Func<IBrowser, IFrame, string, IRequest, IResourceHandler>? SchemeHandler { get; set; }
public CefService(ILogger<CefService> logger)
{
_logger = logger;
Inited = false;
Init();
}
public IWebBrowser CreateBrowser()
{
return new ChromiumWebBrowser();
}
private void Init()
{
if (Inited || Cef.IsInitialized) return;
Inited = true;
var settings = new CefSettings
{
CachePath = Consts.CefCacheLocation.ToString(),
UserAgent = "Wabbajack In-App Browser"
};
settings.RegisterScheme(new CefCustomScheme()
{
SchemeName = "wabbajack",
SchemeHandlerFactory = new SchemeHandlerFactor(_logger, this)
});
_logger.LogInformation("Initializing Cef");
if (!Cef.Initialize(settings))
{
_logger.LogError("Cannot initialize CEF");
}
}
private class SchemeHandlerFactor : ISchemeHandlerFactory
{
private readonly ILogger _logger;
private readonly CefService _service;
internal SchemeHandlerFactor(ILogger logger, CefService service)
{
_logger = logger;
_service = service;
}
public IResourceHandler Create(IBrowser browser, IFrame frame, string schemeName, IRequest request)
{
_logger.LogInformation("Scheme handler Got: {Scheme} : {Url}", schemeName, request.Url);
if (schemeName == "wabbajack")
{
_service._schemeStream.OnNext(request.Url);
}
return new ResourceHandler();
}
}
}

View File

@ -0,0 +1,155 @@
using System;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Reactive.Disposables;
using System.Reactive.Subjects;
using System.Text;
using System.Threading;
using DynamicData;
using Microsoft.Extensions.Logging;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
using Wabbajack.Services.OSIntegrated;
namespace Wabbajack.Models;
public class LoggerProvider : ILoggerProvider
{
private readonly RelativePath _appName;
private readonly Configuration _configuration;
private readonly CompositeDisposable _disposables;
private readonly Stream _logFile;
private readonly StreamWriter _logStream;
public readonly ReadOnlyObservableCollection<ILogMessage> _messagesFiltered;
private readonly DateTime _startupTime;
private long _messageId;
private readonly SourceCache<ILogMessage, long> _messageLog = new(m => m.MessageId);
private readonly Subject<ILogMessage> _messages = new();
public LoggerProvider(Configuration configuration)
{
_startupTime = DateTime.UtcNow;
_configuration = configuration;
_configuration.LogLocation.CreateDirectory();
_disposables = new CompositeDisposable();
Messages
.ObserveOnGuiThread()
.Subscribe(m => _messageLog.AddOrUpdate(m))
.DisposeWith(_disposables);
Messages.Subscribe(m => LogToFile(m))
.DisposeWith(_disposables);
_messageLog.Connect()
.Bind(out _messagesFiltered)
.Subscribe()
.DisposeWith(_disposables);
_messages.DisposeWith(_disposables);
_appName = typeof(LoggerProvider).Assembly.Location.ToAbsolutePath().FileName;
LogPath = _configuration.LogLocation.Combine($"{_appName}.current.log");
_logFile = LogPath.Open(FileMode.Append, FileAccess.Write);
_logFile.DisposeWith(_disposables);
_logStream = new StreamWriter(_logFile, Encoding.UTF8);
}
public IObservable<ILogMessage> Messages => _messages;
public AbsolutePath LogPath { get; }
public ReadOnlyObservableCollection<ILogMessage> MessageLog => _messagesFiltered;
public void Dispose()
{
_disposables.Dispose();
}
public ILogger CreateLogger(string categoryName)
{
return new Logger(this, categoryName);
}
private void LogToFile(ILogMessage logMessage)
{
var line = $"[{logMessage.TimeStamp - _startupTime}] {logMessage.LongMessage}";
lock (_logStream)
{
_logStream.Write(line);
_logStream.Flush();
}
}
private long NextMessageId()
{
return Interlocked.Increment(ref _messageId);
}
public class Logger : ILogger
{
private readonly string _categoryName;
private readonly LoggerProvider _provider;
private ImmutableList<object> Scopes = ImmutableList<object>.Empty;
public Logger(LoggerProvider provider, string categoryName)
{
_categoryName = categoryName;
_provider = provider;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
Func<TState, Exception?, string> formatter)
{
Debug.WriteLine($"{logLevel} - {formatter(state, exception)}");
_provider._messages.OnNext(new LogMessage<TState>(DateTime.UtcNow, _provider.NextMessageId(), logLevel,
eventId, state, exception, formatter));
}
public bool IsEnabled(LogLevel logLevel)
{
return true;
}
public IDisposable BeginScope<TState>(TState state)
{
Scopes = Scopes.Add(state);
return Disposable.Create(() => Scopes = Scopes.Remove(state));
}
}
public interface ILogMessage
{
long MessageId { get; }
string ShortMessage { get; }
DateTime TimeStamp { get; }
string LongMessage { get; }
}
private record LogMessage<TState>(DateTime TimeStamp, long MessageId, LogLevel LogLevel, EventId EventId,
TState State, Exception? Exception, Func<TState, Exception?, string> Formatter) : ILogMessage
{
public string ShortMessage => Formatter(State, Exception);
public string LongMessage
{
get
{
var sb = new StringBuilder();
sb.AppendLine(ShortMessage);
if (Exception != null)
{
sb.Append("Exception: ");
sb.Append(Exception);
}
return sb.ToString();
}
}
}
}

View File

@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Subjects;
using System.Timers;
using DynamicData;
using DynamicData.Kernel;
using ReactiveUI;
using Wabbajack.RateLimiter;
namespace Wabbajack.Models;
public class ResourceMonitor : IDisposable
{
private readonly TimeSpan PollInterval = TimeSpan.FromMilliseconds(1000);
private readonly IResource[] _resources;
private readonly Timer _timer;
private readonly Subject<(string Name, long Througput)[]> _updates = new ();
private (string Name, long Throughput)[] _prev;
public IObservable<(string Name, long Throughput)[]> Updates => _updates;
private readonly SourceCache<CPUDisplayVM, ulong> _tasks = new(x => x.ID);
public readonly ReadOnlyObservableCollection<CPUDisplayVM> _tasksFiltered;
private readonly CompositeDisposable _compositeDisposable;
public ReadOnlyObservableCollection<CPUDisplayVM> Tasks => _tasksFiltered;
public ResourceMonitor(IEnumerable<IResource> resources)
{
_compositeDisposable = new CompositeDisposable();
_resources = resources.ToArray();
_prev = _resources.Select(x => (x.Name, (long)0)).ToArray();
RxApp.MainThreadScheduler.ScheduleRecurringAction(PollInterval, Elapsed)
.DisposeWith(_compositeDisposable);
_tasks.Connect()
.Bind(out _tasksFiltered)
.Subscribe()
.DisposeWith(_compositeDisposable);
}
private void Elapsed()
{
var current = _resources.Select(x => (x.Name, x.StatusReport.Transferred)).ToArray();
var diff = _prev.Zip(current)
.Select(t => (t.First.Name, (long)((t.Second.Transferred - t.First.Throughput) / PollInterval.TotalSeconds)))
.ToArray();
_prev = current;
_updates.OnNext(diff);
_tasks.Edit(l =>
{
var used = new HashSet<ulong>();
foreach (var resource in _resources)
{
foreach (var job in resource.Jobs)
{
used.Add(job.ID);
var tsk = l.Lookup(job.ID);
// Update
if (tsk != Optional<CPUDisplayVM>.None)
{
var t = tsk.Value;
t.Msg = job.Description;
t.ProgressPercent = job.Size == 0 ? Percent.Zero : Percent.FactoryPutInRange(job.Current, (long)job.Size);
}
// Create
else
{
var vm = new CPUDisplayVM
{
ID = job.ID,
StartTime = DateTime.Now,
Msg = job.Description,
ProgressPercent = job.Size == 0 ? Percent.Zero : Percent.FactoryPutInRange(job.Current, (long) job.Size)
};
l.AddOrUpdate(vm);
}
}
}
// Delete
foreach (var itm in l.Items.Where(v => !used.Contains(v.ID)))
l.Remove(itm);
});
}
public void Dispose()
{
_compositeDisposable.Dispose();
}
}

View File

@ -0,0 +1,27 @@
using System.Reflection;
using System.Runtime.InteropServices;
using System.Windows;
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
//In order to begin building localizable applications, set
//<UICulture>CultureYouAreCodingWith</UICulture> in your .csproj file
//inside a <PropertyGroup>. For example, if you are using US english
//in your source files, set the <UICulture> to en-US. Then uncomment
//the NeutralResourceLanguage attribute below. Update the "en-US" in
//the line below to match the UICulture setting in the project file.
//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]

View File

@ -0,0 +1,63 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Wabbajack.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Wabbajack.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
}
}

View File

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -0,0 +1,26 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Wabbajack.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.3.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default {
get {
return defaultInstance;
}
}
}
}

View File

@ -0,0 +1,8 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>

View File

@ -0,0 +1,4 @@
# Wabbajack
This is the main project of this solution. It contains only UI code like all Views and View Models as well as custom Components and Resources such as icons and images.
You can consider this project to be the front end of Wabbajack.

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -0,0 +1,20 @@
using System;
using System.Windows;
using System.Windows.Media.Imaging;
namespace Wabbajack
{
public static class ResourceLinks
{
public static Lazy<BitmapImage> WabbajackLogo { get; } = new Lazy<BitmapImage>(() =>
UIUtils.BitmapImageFromStream(Application.GetResourceStream(new Uri("pack://application:,,,/Resources/Wabba_Mouth.png")).Stream));
public static Lazy<BitmapImage> WabbajackLogoNoText { get; } = new Lazy<BitmapImage>(() =>
UIUtils.BitmapImageFromStream(Application.GetResourceStream(new Uri("pack://application:,,,/Resources/Wabba_Mouth_No_Text.png")).Stream));
public static Lazy<BitmapImage> WabbajackErrLogo { get; } = new Lazy<BitmapImage>(() =>
UIUtils.BitmapImageFromStream(Application.GetResourceStream(new Uri("pack://application:,,,/Resources/Wabba_Ded.png")).Stream));
public static Lazy<BitmapImage> MO2Button { get; } = new Lazy<BitmapImage>(() =>
UIUtils.BitmapImageFromStream(Application.GetResourceStream(new Uri("pack://application:,,,/Resources/MO2Button.png")).Stream));
public static Lazy<BitmapImage> MiddleMouseButton { get; } = new Lazy<BitmapImage>(() =>
UIUtils.BitmapImageFromStream(Application.GetResourceStream(new Uri("pack://application:,,,/Resources/middle_mouse_button.png")).Stream));
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,191 @@
using System;
using System.Collections.Generic;
using System.Reactive;
using System.Reactive.Subjects;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Wabbajack.Compiler;
using Wabbajack.DTOs.JsonConverters;
using Wabbajack;
using Wabbajack.Paths;
using Consts = Wabbajack.Consts;
namespace Wabbajack
{
[JsonName("MainSettings")]
[JsonObject(MemberSerialization.OptOut)]
public class MainSettings
{
public byte Version { get; set; } = Consts.SettingsVersion;
public double PosX { get; set; }
public double PosY { get; set; }
public double Height { get; set; }
public double Width { get; set; }
public InstallerSettings Installer { get; set; } = new InstallerSettings();
public FiltersSettings Filters { get; set; } = new FiltersSettings();
public CompilerSettings Compiler { get; set; } = new CompilerSettings();
public PerformanceSettings Performance { get; set; } = new PerformanceSettings();
private Subject<Unit> _saveSignal = new Subject<Unit>();
[JsonIgnore]
public IObservable<Unit> SaveSignal => _saveSignal;
public static async ValueTask<(MainSettings settings, bool loaded)> TryLoadTypicalSettings()
{
/*
if (!Consts.SettingsFile.Exists)
{
return default;
}
// Version check
try
{
var settings = Consts.SettingsFile.FromJson<MainSettings>();
if (settings.Version == Consts.SettingsVersion)
return (settings, true);
}
catch (Exception ex)
{
Utils.Error(ex, "Error loading settings");
}
var backup = Consts.SettingsFile.AppendToName("-backup");
await backup.DeleteAsync();
await Consts.SettingsFile.CopyToAsync(backup);
await Consts.SettingsFile.DeleteAsync();
*/
return default;
}
public static async ValueTask SaveSettings(MainSettings settings)
{
settings._saveSignal.OnNext(Unit.Default);
// Might add this if people are putting save work on other threads or other
// things that delay the operation.
//settings._saveSignal.OnCompleted();
//await settings._saveSignal;
//await settings.ToJsonAsync(Consts.SettingsFile);
}
}
[JsonName("InstallerSettings")]
public class InstallerSettings
{
public AbsolutePath LastInstalledListLocation { get; set; }
public Dictionary<AbsolutePath, Mo2ModlistInstallationSettings> Mo2ModlistSettings { get; } = new Dictionary<AbsolutePath, Mo2ModlistInstallationSettings>();
}
[JsonName("Mo2ModListInstallerSettings")]
public class Mo2ModlistInstallationSettings
{
public AbsolutePath InstallationLocation { get; set; }
public AbsolutePath DownloadLocation { get; set; }
public bool AutomaticallyOverrideExistingInstall { get; set; }
}
[JsonName("FiltersSettings")]
[JsonObject(MemberSerialization.OptOut)]
public class FiltersSettings : ViewModel
{
public bool ShowNSFW { get; set; }
public bool OnlyInstalled { get; set; }
public string Game { get; set; }
public string Search { get; set; }
private bool _isPersistent = true;
public bool IsPersistent { get => _isPersistent; set => RaiseAndSetIfChanged(ref _isPersistent, value); }
private bool _useCompression = false;
public bool UseCompression { get => _useCompression; set => RaiseAndSetIfChanged(ref _useCompression, value); }
public bool ShowUtilityLists { get; set; }
}
[JsonName("PerformanceSettings")]
[JsonObject(MemberSerialization.OptOut)]
public class PerformanceSettings : ViewModel
{
public PerformanceSettings()
{
_reduceHDDThreads = true;
_favorPerfOverRam = false;
_diskThreads = Environment.ProcessorCount;
_downloadThreads = Environment.ProcessorCount <= 8 ? Environment.ProcessorCount : 8;
}
private int _downloadThreads;
public int DownloadThreads { get => _downloadThreads; set => RaiseAndSetIfChanged(ref _downloadThreads, value); }
private int _diskThreads;
public int DiskThreads { get => _diskThreads; set => RaiseAndSetIfChanged(ref _diskThreads, value); }
private bool _reduceHDDThreads;
public bool ReduceHDDThreads { get => _reduceHDDThreads; set => RaiseAndSetIfChanged(ref _reduceHDDThreads, value); }
private bool _favorPerfOverRam;
public bool FavorPerfOverRam { get => _favorPerfOverRam; set => RaiseAndSetIfChanged(ref _favorPerfOverRam, value); }
private bool _networkWorkaroundMode;
public bool NetworkWorkaroundMode
{
get => _networkWorkaroundMode;
set
{
Consts.UseNetworkWorkaroundMode = value;
RaiseAndSetIfChanged(ref _networkWorkaroundMode, value);
}
}
private bool _disableTextureResizing;
public bool DisableTextureResizing
{
get => _disableTextureResizing;
set
{
RaiseAndSetIfChanged(ref _disableTextureResizing, value);
}
}
/*
public void SetProcessorSettings(ABatchProcessor processor)
{
processor.DownloadThreads = DownloadThreads;
processor.DiskThreads = DiskThreads;
processor.ReduceHDDThreads = ReduceHDDThreads;
processor.FavorPerfOverRam = FavorPerfOverRam;
if (processor is MO2Compiler mo2c)
mo2c.DisableTextureResizing = DisableTextureResizing;
}*/
}
[JsonName("CompilationModlistSettings")]
public class CompilationModlistSettings
{
public string ModListName { get; set; }
public string Version { get; set; }
public string Author { get; set; }
public string Description { get; set; }
public string Website { get; set; }
public string Readme { get; set; }
public bool IsNSFW { get; set; }
public string MachineUrl { get; set; }
public AbsolutePath SplashScreen { get; set; }
public bool Publish { get; set; }
}
[JsonName("MO2CompilationSettings")]
public class MO2CompilationSettings
{
public AbsolutePath DownloadLocation { get; set; }
public AbsolutePath LastCompiledProfileLocation { get; set; }
public Dictionary<AbsolutePath, CompilationModlistSettings> ModlistSettings { get; } = new Dictionary<AbsolutePath, CompilationModlistSettings>();
}
}

View File

@ -0,0 +1,20 @@
using Wabbajack.Paths;
namespace Wabbajack
{
public class ConfirmUpdateOfExistingInstall : ConfirmationIntervention
{
public AbsolutePath OutputFolder { get; set; }
public string ModListName { get; set; } = string.Empty;
public override string ShortDescription { get; } = "Do you want to update existing files?";
public override string ExtendedDescription
{
get =>
$@"There appears to be a modlist already installed in the output folder. If you continue with the install,
Any files that exist in {OutputFolder} will be changed to match the files found in the {ModListName} modlist. Custom settings
will be reverted, but saved games will not be deleted. Are you sure you wish to continue?";
}
}
}

View File

@ -0,0 +1,31 @@
using System.Threading.Tasks;
using Wabbajack.Common;
using Wabbajack.Interventions;
namespace Wabbajack
{
/// <summary>
/// This should probably be replaced with an error, but this is just to get messageboxes out of the .Lib library
/// </summary>
public class CriticalFailureIntervention : AErrorMessage
{
private TaskCompletionSource<ConfirmationIntervention.Choice> _source = new TaskCompletionSource<ConfirmationIntervention.Choice>();
public Task<ConfirmationIntervention.Choice> Task => _source.Task;
public CriticalFailureIntervention(string description, string title, bool exit = false)
{
ExtendedDescription = description;
ShortDescription = title;
ExitApplication = exit;
}
public override string ShortDescription { get; }
public override string ExtendedDescription { get; }
public bool ExitApplication { get; }
public void Cancel()
{
_source.SetResult(ConfirmationIntervention.Choice.Abort);
}
}
}

View File

@ -0,0 +1,37 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Wabbajack.DTOs.DownloadStates;
namespace Wabbajack
{
public class ManuallyDownloadFile : AUserIntervention
{
public Manual State { get; }
public override string ShortDescription { get; } = string.Empty;
public override string ExtendedDescription { get; } = string.Empty;
private readonly TaskCompletionSource<(Uri, HttpResponseMessage)> _tcs = new ();
public Task<(Uri, HttpResponseMessage)> Task => _tcs.Task;
private ManuallyDownloadFile(Manual state)
{
State = state;
}
public static async Task<ManuallyDownloadFile> Create(Manual state)
{
var result = new ManuallyDownloadFile(state);
return result;
}
public override void Cancel()
{
_tcs.SetCanceled();
}
public void Resume(Uri s, HttpResponseMessage client)
{
_tcs.SetResult((s, client));
}
}
}

View File

@ -0,0 +1,36 @@
using System;
using System.Threading.Tasks;
using Wabbajack.DTOs.DownloadStates;
namespace Wabbajack
{
public class ManuallyDownloadNexusFile : AUserIntervention
{
public Nexus State { get; }
public override string ShortDescription { get; } = string.Empty;
public override string ExtendedDescription { get; } = string.Empty;
private TaskCompletionSource<Uri> _tcs = new TaskCompletionSource<Uri>();
public Task<Uri> Task => _tcs.Task;
private ManuallyDownloadNexusFile(Nexus state)
{
State = state;
}
public static async Task<ManuallyDownloadNexusFile> Create(Nexus state)
{
var result = new ManuallyDownloadNexusFile(state);
return result;
}
public override void Cancel()
{
_tcs.SetCanceled();
}
public void Resume(Uri s)
{
_tcs.SetResult(s);
}
}
}

View File

@ -0,0 +1,12 @@
using Wabbajack.Interventions;
namespace Wabbajack
{
public class NexusAPIQuotaExceeded : AErrorMessage
{
public override string ShortDescription => $"You have exceeded your Nexus API limit for the day";
public override string ExtendedDescription =>
"You have exceeded your Nexus API limit for the day, please try again after midnight GMT";
}
}

View File

@ -0,0 +1,15 @@
using Wabbajack.Common;
namespace Wabbajack
{
public class YesNoIntervention : ConfirmationIntervention
{
public YesNoIntervention(string description, string title)
{
ExtendedDescription = description;
ShortDescription = title;
}
public override string ShortDescription { get; }
public override string ExtendedDescription { get; }
}
}

View File

@ -0,0 +1,118 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Wabbajack">
<Style TargetType="local:AttentionBorder">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:AttentionBorder">
<Border BorderThickness="1">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="{StaticResource WindowBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource DarkerSecondaryBrush}" />
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsVisible, RelativeSource={RelativeSource Self}}" Value="True" />
<Condition Binding="{Binding IsMouseOver, RelativeSource={RelativeSource Self}}" Value="False" />
<Condition Binding="{Binding Failure, RelativeSource={RelativeSource AncestorType={x:Type local:AttentionBorder}}}" Value="False" />
</MultiDataTrigger.Conditions>
<MultiDataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
AutoReverse="True"
RepeatBehavior="Forever"
Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)"
To="{StaticResource Secondary}"
Duration="0:0:1.5" />
</Storyboard>
</BeginStoryboard>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
AutoReverse="True"
RepeatBehavior="Forever"
Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
To="{StaticResource SecondaryBackground}"
Duration="0:0:1.5" />
</Storyboard>
</BeginStoryboard>
</MultiDataTrigger.EnterActions>
<MultiDataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)"
To="{StaticResource DarkerSecondary}"
Duration="0:0:0.1" />
</Storyboard>
</BeginStoryboard>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
To="{StaticResource WindowBackgroundColor}"
Duration="0:0:0.1" />
</Storyboard>
</BeginStoryboard>
</MultiDataTrigger.ExitActions>
</MultiDataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsVisible, RelativeSource={RelativeSource Self}}" Value="True" />
<Condition Binding="{Binding IsMouseOver, RelativeSource={RelativeSource Self}}" Value="False" />
<Condition Binding="{Binding Failure, RelativeSource={RelativeSource AncestorType={x:Type local:AttentionBorder}}}" Value="True" />
</MultiDataTrigger.Conditions>
<MultiDataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
AutoReverse="True"
RepeatBehavior="Forever"
Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)"
To="#ff0026"
Duration="0:0:1.5" />
</Storyboard>
</BeginStoryboard>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
AutoReverse="True"
RepeatBehavior="Forever"
Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
To="#540914"
Duration="0:0:1.5" />
</Storyboard>
</BeginStoryboard>
</MultiDataTrigger.EnterActions>
<MultiDataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)"
To="#700d1c"
Duration="0:0:0.1" />
</Storyboard>
</BeginStoryboard>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
To="#1c0307"
Duration="0:0:0.1" />
</Storyboard>
</BeginStoryboard>
</MultiDataTrigger.ExitActions>
</MultiDataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<ContentPresenter Content="{TemplateBinding Content}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,16 @@
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Wabbajack.Models;
using Wabbajack.Networking.Http.Interfaces;
namespace Wabbajack.UserIntervention;
public class LoversLabLoginHandler : OAuth2LoginHandler<Messages.LoversLabLogin, DTOs.Logins.LoversLabLoginState>
{
public LoversLabLoginHandler(ILogger<LoversLabLoginHandler> logger, HttpClient client, ITokenProvider<DTOs.Logins.LoversLabLoginState> tokenProvider,
WebBrowserVM browser, CefService service)
: base(logger, client, tokenProvider, browser, service)
{
}
}

View File

@ -0,0 +1,107 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ReactiveUI;
using Wabbajack.DTOs.Logins;
using Wabbajack.LibCefHelpers;
using Wabbajack.Messages;
using Wabbajack.Models;
using Wabbajack.Networking.Http.Interfaces;
using Wabbajack.Services.OSIntegrated.TokenProviders;
namespace Wabbajack.UserIntervention;
public class NexusLoginHandler : WebUserInterventionBase<NexusLogin>
{
private readonly ITokenProvider<NexusApiState> _provider;
public NexusLoginHandler(ILogger<NexusLoginHandler> logger, WebBrowserVM browserVM, ITokenProvider<NexusApiState> provider, CefService service)
: base(logger, browserVM, service)
{
_provider = provider;
}
public override async Task Begin()
{
try
{
Messages.NavigateTo.Send(Browser);
UpdateStatus("Please log into the Nexus");
await Driver.WaitForInitialized();
await NavigateTo(new Uri("https://users.nexusmods.com/auth/continue?client_id=nexus&redirect_uri=https://www.nexusmods.com/oauth/callback&response_type=code&referrer=//www.nexusmods.com"));
Cookie[] cookies = {};
while (true)
{
cookies = await Driver.GetCookies("nexusmods.com");
if (cookies.Any(c => c.Name == "member_id"))
break;
Message.Token.ThrowIfCancellationRequested();
await Task.Delay(500, Message.Token);
}
await NavigateTo(new Uri("https://www.nexusmods.com/users/myaccount?tab=api"));
UpdateStatus("Looking for API Key");
var key = "";
while (true)
{
try
{
key = await Driver.EvaluateJavaScript(
"document.querySelector(\"input[value=wabbajack]\").parentElement.parentElement.querySelector(\"textarea.application-key\").innerHTML");
}
catch (Exception)
{
// ignored
}
if (!string.IsNullOrEmpty(key))
{
break;
}
try
{
await Driver.EvaluateJavaScript(
"var found = document.querySelector(\"input[value=wabbajack]\").parentElement.parentElement.querySelector(\"form button[type=submit]\");" +
"found.onclick= function() {return true;};" +
"found.class = \" \"; " +
"found.click();" +
"found.remove(); found = undefined;"
);
UpdateStatus("Generating API Key, Please Wait...");
}
catch (Exception)
{
// ignored
}
Message.Token.ThrowIfCancellationRequested();
await Task.Delay(500, Message.Token);
}
await _provider.SetToken(new NexusApiState()
{
ApiKey = key,
Cookies = cookies
});
((NexusLogin)Message).CompletionSource.SetResult();
Messages.NavigateTo.Send(PrevPane);
}
catch (Exception ex)
{
Logger.LogError(ex, "While logging into Nexus Mods");
Message.SetException(ex);
Messages.NavigateTo.Send(PrevPane);
}
}
}

View File

@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Microsoft.Extensions.Logging;
using ReactiveUI;
using Wabbajack.DTOs.Interventions;
using Wabbajack.DTOs.Logins;
using Wabbajack.Messages;
using Wabbajack.Models;
using Wabbajack.Networking.Http.Interfaces;
using Wabbajack.Services.OSIntegrated;
namespace Wabbajack.UserIntervention;
public abstract class OAuth2LoginHandler<TIntervention, TLoginType> : WebUserInterventionBase<TIntervention>
where TIntervention : IUserIntervention
where TLoginType : OAuth2LoginState, new()
{
private readonly HttpClient _httpClient;
private readonly ITokenProvider<TLoginType> _tokenProvider;
public OAuth2LoginHandler(ILogger logger, HttpClient httpClient,
ITokenProvider<TLoginType> tokenProvider, WebBrowserVM browserVM, CefService service) : base(logger, browserVM, service)
{
_httpClient = httpClient;
_tokenProvider = tokenProvider;
}
public override async Task Begin()
{
Messages.NavigateTo.Send(Browser);
var tlogin = new TLoginType();
await Driver.WaitForInitialized();
using var handler = Driver.WithSchemeHandler(uri => uri.Scheme == "wabbajack");
UpdateStatus($"Please log in and allow Wabbajack to access your {tlogin.SiteName} account");
var scopes = string.Join(" ", tlogin.Scopes);
var state = Guid.NewGuid().ToString();
await NavigateTo(new Uri(tlogin.AuthorizationEndpoint + $"?response_type=code&client_id={tlogin.ClientID}&state={state}&scope={scopes}"));
var uri = await handler.Task.WaitAsync(Message.Token);
var cookies = await Driver.GetCookies(tlogin.AuthorizationEndpoint.Host);
var parsed = HttpUtility.ParseQueryString(uri.Query);
if (parsed.Get("state") != state)
{
Logger.LogCritical("Bad OAuth state, this shouldn't happen");
throw new Exception("Bad OAuth State");
}
if (parsed.Get("code") == null)
{
Logger.LogCritical("Bad code result from OAuth");
throw new Exception("Bad code result from OAuth");
}
var authCode = parsed.Get("code");
var formData = new KeyValuePair<string?, string?>[]
{
new("grant_type", "authorization_code"),
new("code", authCode),
new("client_id", tlogin.ClientID)
};
var msg = new HttpRequestMessage();
msg.Method = HttpMethod.Post;
msg.RequestUri = tlogin.TokenEndpoint;
msg.Headers.Add("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36");
msg.Headers.Add("Cookie", string.Join(";", cookies.Select(c => $"{c.Name}={c.Value}")));
msg.Content = new FormUrlEncodedContent(formData.ToList());
using var response = await _httpClient.SendAsync(msg, Message.Token);
var data = await response.Content.ReadFromJsonAsync<OAuthResultState>(cancellationToken: Message.Token);
await _tokenProvider.SetToken(new TLoginType
{
Cookies = cookies,
ResultState = data!
});
Messages.NavigateTo.Send(PrevPane);
}
}

View File

@ -0,0 +1,15 @@
using System.Net.Http;
using Microsoft.Extensions.Logging;
using Wabbajack.Models;
using Wabbajack.Networking.Http.Interfaces;
namespace Wabbajack.UserIntervention;
public class VectorPlexusLoginHandler : OAuth2LoginHandler<Messages.VectorPlexusLogin, DTOs.Logins.VectorPlexusLoginState>
{
public VectorPlexusLoginHandler(ILogger<VectorPlexusLoginHandler> logger, HttpClient client, ITokenProvider<DTOs.Logins.VectorPlexusLoginState> tokenProvider,
WebBrowserVM browser, CefService service)
: base(logger, client, tokenProvider, browser, service)
{
}
}

View File

@ -0,0 +1,46 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Wabbajack.DTOs.Interventions;
using Wabbajack.Interventions;
using Wabbajack.Models;
using Wabbajack.WebAutomation;
namespace Wabbajack.UserIntervention;
public abstract class WebUserInterventionBase<T>
where T : IUserIntervention
{
protected readonly WebBrowserVM Browser;
protected readonly ILogger Logger;
protected T Message;
protected ViewModel PrevPane;
protected IWebDriver Driver;
protected WebUserInterventionBase(ILogger logger, WebBrowserVM browser, CefService service)
{
Logger = logger;
Browser = browser;
Driver = new CefSharpWrapper(logger, browser.Browser, service);
}
public void Configure(ViewModel prevPane, T message)
{
Message = message;
PrevPane = prevPane;
}
protected void UpdateStatus(string status)
{
Browser.Instructions = status;
}
protected async Task NavigateTo(Uri uri)
{
await Driver.NavigateTo(uri, Message.Token);
}
public abstract Task Begin();
}

View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Wabbajack
{
public class AsyncLazy<T> : Lazy<Task<T>>
{
public AsyncLazy(Func<T> valueFactory) :
base(() => Task.Factory.StartNew(valueFactory))
{
}
public AsyncLazy(Func<Task<T>> taskFactory) :
base(() => Task.Factory.StartNew(() => taskFactory()).Unwrap())
{
}
}
}

View File

@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
namespace Wabbajack
{
internal class AutoScrollBehavior
{
private static readonly Dictionary<ListBox, Capture> Associations =
new Dictionary<ListBox, Capture>();
public static readonly DependencyProperty ScrollOnNewItemProperty =
DependencyProperty.RegisterAttached(
"ScrollOnNewItem",
typeof(bool),
typeof(AutoScrollBehavior),
new UIPropertyMetadata(false, OnScrollOnNewItemChanged));
public static bool GetScrollOnNewItem(DependencyObject obj)
{
return (bool)obj.GetValue(ScrollOnNewItemProperty);
}
public static void SetScrollOnNewItem(DependencyObject obj, bool value)
{
obj.SetValue(ScrollOnNewItemProperty, value);
}
public static void OnScrollOnNewItemChanged(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var listBox = d as ListBox;
if (listBox == null) return;
bool oldValue = (bool)e.OldValue, newValue = (bool)e.NewValue;
if (newValue == oldValue) return;
if (newValue)
{
listBox.Loaded += ListBox_Loaded;
listBox.Unloaded += ListBox_Unloaded;
var itemsSourcePropertyDescriptor = TypeDescriptor.GetProperties(listBox)["ItemsSource"];
itemsSourcePropertyDescriptor.AddValueChanged(listBox, ListBox_ItemsSourceChanged);
}
else
{
listBox.Loaded -= ListBox_Loaded;
listBox.Unloaded -= ListBox_Unloaded;
if (Associations.ContainsKey(listBox))
Associations[listBox].Dispose();
var itemsSourcePropertyDescriptor = TypeDescriptor.GetProperties(listBox)["ItemsSource"];
itemsSourcePropertyDescriptor.RemoveValueChanged(listBox, ListBox_ItemsSourceChanged);
}
}
private static void ListBox_ItemsSourceChanged(object sender, EventArgs e)
{
var listBox = (ListBox)sender;
if (Associations.ContainsKey(listBox))
Associations[listBox].Dispose();
Associations[listBox] = new Capture(listBox);
}
private static void ListBox_Unloaded(object sender, RoutedEventArgs e)
{
var listBox = (ListBox)sender;
if (Associations.ContainsKey(listBox))
Associations[listBox].Dispose();
listBox.Unloaded -= ListBox_Unloaded;
}
private static void ListBox_Loaded(object sender, RoutedEventArgs e)
{
var listBox = (ListBox)sender;
var incc = listBox.Items as INotifyCollectionChanged;
if (incc == null) return;
listBox.Loaded -= ListBox_Loaded;
Associations[listBox] = new Capture(listBox);
}
private class Capture : IDisposable
{
private readonly INotifyCollectionChanged _incc;
private readonly ListBox _listBox;
public Capture(ListBox listBox)
{
this._listBox = listBox;
_incc = listBox.ItemsSource as INotifyCollectionChanged;
if (_incc != null) _incc.CollectionChanged += incc_CollectionChanged;
}
public void Dispose()
{
if (_incc != null)
_incc.CollectionChanged -= incc_CollectionChanged;
}
private void incc_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
try
{
_listBox.ScrollIntoView(e.NewItems[0]);
_listBox.SelectedItem = e.NewItems[0];
}
catch (ArgumentOutOfRangeException) { }
}
}
}
}
}

View File

@ -0,0 +1,278 @@
using DynamicData;
using Microsoft.WindowsAPICodePack.Dialogs;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using System;
using System.Linq;
using System.Reactive.Linq;
using System.Windows.Input;
using Wabbajack;
using Wabbajack.Extensions;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
namespace Wabbajack
{
public class FilePickerVM : ViewModel
{
public enum PathTypeOptions
{
Off,
Either,
File,
Folder
}
public enum CheckOptions
{
Off,
IfPathNotEmpty,
On
}
public object Parent { get; }
[Reactive]
public ICommand SetTargetPathCommand { get; set; }
[Reactive]
public AbsolutePath TargetPath { get; set; }
[Reactive]
public string PromptTitle { get; set; }
[Reactive]
public PathTypeOptions PathType { get; set; }
[Reactive]
public CheckOptions ExistCheckOption { get; set; }
[Reactive]
public CheckOptions FilterCheckOption { get; set; } = CheckOptions.IfPathNotEmpty;
[Reactive]
public IObservable<IErrorResponse> AdditionalError { get; set; }
private readonly ObservableAsPropertyHelper<bool> _exists;
public bool Exists => _exists.Value;
private readonly ObservableAsPropertyHelper<ErrorResponse> _errorState;
public ErrorResponse ErrorState => _errorState.Value;
private readonly ObservableAsPropertyHelper<bool> _inError;
public bool InError => _inError.Value;
private readonly ObservableAsPropertyHelper<string> _errorTooltip;
public string ErrorTooltip => _errorTooltip.Value;
public SourceList<CommonFileDialogFilter> Filters { get; } = new();
public const string PathDoesNotExistText = "Path does not exist";
public const string DoesNotPassFiltersText = "Path does not pass designated filters";
public FilePickerVM(object parentVM = null)
{
Parent = parentVM;
SetTargetPathCommand = ConstructTypicalPickerCommand();
var existsCheckTuple = Observable.CombineLatest(
this.WhenAny(x => x.ExistCheckOption),
this.WhenAny(x => x.PathType),
this.WhenAny(x => x.TargetPath)
// Dont want to debounce the initial value, because we know it's null
.Skip(1)
.Debounce(TimeSpan.FromMilliseconds(200), RxApp.MainThreadScheduler)
.StartWith(default(AbsolutePath)),
resultSelector: (existsOption, type, path) => (ExistsOption: existsOption, Type: type, Path: path))
.StartWith((ExistsOption: ExistCheckOption, Type: PathType, Path: TargetPath))
.Replay(1)
.RefCount();
var doExistsCheck = existsCheckTuple
.Select(t =>
{
// Don't do exists type if we don't know what path type we're tracking
if (t.Type == PathTypeOptions.Off) return false;
switch (t.ExistsOption)
{
case CheckOptions.Off:
return false;
case CheckOptions.IfPathNotEmpty:
return t.Path != default;
case CheckOptions.On:
return true;
default:
throw new NotImplementedException();
}
})
.Replay(1)
.RefCount();
_exists = Observable.Interval(TimeSpan.FromSeconds(3), RxApp.TaskpoolScheduler)
// Only check exists on timer if desired
.FlowSwitch(doExistsCheck)
.Unit()
// Also check though, when fields change
.Merge(this.WhenAny(x => x.PathType).Unit())
.Merge(this.WhenAny(x => x.ExistCheckOption).Unit())
.Merge(this.WhenAny(x => x.TargetPath).Unit())
// Signaled to check, get latest params for actual use
.CombineLatest(existsCheckTuple,
resultSelector: (_, tuple) => tuple)
// Refresh exists
.ObserveOn(RxApp.TaskpoolScheduler)
.Select(t =>
{
switch (t.ExistsOption)
{
case CheckOptions.IfPathNotEmpty:
if (t.Path == default) return false;
break;
case CheckOptions.On:
break;
case CheckOptions.Off:
default:
return false;
}
switch (t.Type)
{
case PathTypeOptions.Either:
return t.Path.FileExists() || t.Path.DirectoryExists();
case PathTypeOptions.File:
return t.Path.FileExists();
case PathTypeOptions.Folder:
return t.Path.DirectoryExists();
case PathTypeOptions.Off:
default:
return false;
}
})
.DistinctUntilChanged()
.StartWith(false)
.ToGuiProperty(this, nameof(Exists));
var passesFilters = Observable.CombineLatest(
this.WhenAny(x => x.TargetPath),
this.WhenAny(x => x.PathType),
this.WhenAny(x => x.FilterCheckOption),
Filters.Connect().QueryWhenChanged(),
resultSelector: (target, type, checkOption, query) =>
{
Console.WriteLine("fff");
switch (type)
{
case PathTypeOptions.Either:
case PathTypeOptions.File:
break;
default:
return true;
}
if (query.Count == 0) return true;
switch (checkOption)
{
case CheckOptions.Off:
return true;
case CheckOptions.IfPathNotEmpty:
if (target == default) return true;
break;
case CheckOptions.On:
break;
default:
throw new NotImplementedException();
}
try
{
if (!query.Any(filter => filter.Extensions.Any(ext => new Extension("." + ext) == target.Extension))) return false;
}
catch (ArgumentException)
{
return false;
}
return true;
})
.StartWith(true)
.Select(passed =>
{
if (passed) return ErrorResponse.Success;
return ErrorResponse.Fail(DoesNotPassFiltersText);
})
.Replay(1)
.RefCount();
_errorState = Observable.CombineLatest(
Observable.CombineLatest(
this.WhenAny(x => x.Exists),
doExistsCheck,
resultSelector: (exists, doExists) => !doExists || exists)
.Select(exists => ErrorResponse.Create(successful: exists, exists ? default(string) : PathDoesNotExistText)),
passesFilters,
this.WhenAny(x => x.AdditionalError)
.Select(x => x ?? Observable.Return<IErrorResponse>(ErrorResponse.Success))
.Switch(),
resultSelector: (existCheck, filter, err) =>
{
if (existCheck.Failed) return existCheck;
if (filter.Failed) return filter;
return ErrorResponse.Convert(err);
})
.ToGuiProperty(this, nameof(ErrorState));
_inError = this.WhenAny(x => x.ErrorState)
.Select(x => !x.Succeeded)
.ToGuiProperty(this, nameof(InError));
// Doesn't derive from ErrorState, as we want to bubble non-empty tooltips,
// which is slightly different logic
_errorTooltip = Observable.CombineLatest(
Observable.CombineLatest(
this.WhenAny(x => x.Exists),
doExistsCheck,
resultSelector: (exists, doExists) => !doExists || exists)
.Select(exists => exists ? default(string) : PathDoesNotExistText),
passesFilters
.Select(x => x.Reason),
this.WhenAny(x => x.AdditionalError)
.Select(x => x ?? Observable.Return<IErrorResponse>(ErrorResponse.Success))
.Switch(),
resultSelector: (exists, filters, err) =>
{
if (!string.IsNullOrWhiteSpace(exists)) return exists;
if (!string.IsNullOrWhiteSpace(filters)) return filters;
return err?.Reason;
})
.ToGuiProperty(this, nameof(ErrorTooltip));
}
public ICommand ConstructTypicalPickerCommand(IObservable<bool> canExecute = null)
{
return ReactiveCommand.Create(
execute: () =>
{
AbsolutePath dirPath;
dirPath = TargetPath.FileExists() ? TargetPath.Parent : TargetPath;
var dlg = new CommonOpenFileDialog
{
Title = PromptTitle,
IsFolderPicker = PathType == PathTypeOptions.Folder,
InitialDirectory = dirPath.ToString(),
AddToMostRecentlyUsedList = false,
AllowNonFileSystemItems = false,
DefaultDirectory = dirPath.ToString(),
EnsureFileExists = true,
EnsurePathExists = true,
EnsureReadOnly = false,
EnsureValidNames = true,
Multiselect = false,
ShowPlacesList = true,
};
foreach (var filter in Filters.Items)
{
dlg.Filters.Add(filter);
}
if (dlg.ShowDialog() != CommonFileDialogResult.Ok) return;
TargetPath = (AbsolutePath)dlg.FileName;
}, canExecute: canExecute);
}
}
}

View File

@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Extensions.Logging;
using PInvoke;
using Silk.NET.Core.Native;
using Silk.NET.DXGI;
using Wabbajack.Common;
using Wabbajack.Installer;
using Wabbajack;
using static PInvoke.User32;
using UnmanagedType = System.Runtime.InteropServices.UnmanagedType;
namespace Wabbajack.Util
{
// Much of the GDI code here is taken from : https://github.com/ModOrganizer2/modorganizer/blob/master/src/envmetrics.cpp
// Thanks to MO2 for being good citizens and supporting OSS code
public class SystemParametersConstructor
{
private readonly ILogger<SystemParametersConstructor> _logger;
public SystemParametersConstructor(ILogger<SystemParametersConstructor> logger)
{
_logger = logger;
}
private IEnumerable<(int Width, int Height, bool IsPrimary)> GetDisplays()
{
// Needed to make sure we get the right values from this call
SetProcessDPIAware();
unsafe
{
var col = new List<(int Width, int Height, bool IsPrimary)>();
EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero,
delegate(IntPtr hMonitor, IntPtr hdcMonitor, RECT* lprcMonitor, void *dwData)
{
MONITORINFOEX mi = new MONITORINFOEX();
mi.cbSize = Marshal.SizeOf(mi);
bool success = GetMonitorInfo(hMonitor, (MONITORINFO*)&mi);
if (success)
{
col.Add(((mi.Monitor.right - mi.Monitor.left), (mi.Monitor.bottom - mi.Monitor.top), mi.Flags == MONITORINFO_Flags.MONITORINFOF_PRIMARY));
}
return true;
}, IntPtr.Zero);
return col;
}
}
public SystemParameters Create()
{
var (width, height, _) = GetDisplays().First(d => d.IsPrimary);
/*using var f = new SharpDX.DXGI.Factory1();
var video_memory = f.Adapters1.Select(a =>
Math.Max(a.Description.DedicatedSystemMemory, (long)a.Description.DedicatedVideoMemory)).Max();*/
var dxgiMemory = 0UL;
unsafe
{
using var api = DXGI.GetApi();
IDXGIFactory1* factory1 = default;
try
{
//https://docs.microsoft.com/en-us/windows/win32/api/dxgi/nf-dxgi-createdxgifactory1
SilkMarshal.ThrowHResult(api.CreateDXGIFactory1(SilkMarshal.GuidPtrOf<IDXGIFactory1>(), (void**)&factory1));
uint i = 0u;
while (true)
{
IDXGIAdapter1* adapter1 = default;
//https://docs.microsoft.com/en-us/windows/win32/api/dxgi/nf-dxgi-idxgifactory1-enumadapters1
var res = factory1->EnumAdapters1(i, &adapter1);
var exception = Marshal.GetExceptionForHR(res);
if (exception != null) break;
AdapterDesc1 adapterDesc = default;
//https://docs.microsoft.com/en-us/windows/win32/api/dxgi/nf-dxgi-idxgiadapter1-getdesc1
SilkMarshal.ThrowHResult(adapter1->GetDesc1(&adapterDesc));
var systemMemory = (ulong) adapterDesc.DedicatedSystemMemory;
var videoMemory = (ulong) adapterDesc.DedicatedVideoMemory;
var maxMemory = Math.Max(systemMemory, videoMemory);
if (maxMemory > dxgiMemory)
dxgiMemory = maxMemory;
adapter1->Release();
i++;
}
}
catch (Exception e)
{
_logger.LogError(e, "While getting SystemParameters");
}
finally
{
if (factory1->LpVtbl != (void**)IntPtr.Zero)
factory1->Release();
}
}
var memory = GetMemoryStatus();
return new SystemParameters
{
ScreenWidth = width,
ScreenHeight = height,
VideoMemorySize = (long)dxgiMemory,
SystemMemorySize = (long)memory.ullTotalPhys,
SystemPageSize = (long)memory.ullTotalPageFile - (long)memory.ullTotalPhys
};
}
[return: MarshalAs(UnmanagedType.Bool)]
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern bool GlobalMemoryStatusEx([In, Out] MEMORYSTATUSEX lpBuffer);
public static MEMORYSTATUSEX GetMemoryStatus()
{
var mstat = new MEMORYSTATUSEX();
GlobalMemoryStatusEx(mstat);
return mstat;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public class MEMORYSTATUSEX
{
public uint dwLength;
public uint dwMemoryLoad;
public ulong ullTotalPhys;
public ulong ullAvailPhys;
public ulong ullTotalPageFile;
public ulong ullAvailPageFile;
public ulong ullTotalVirtual;
public ulong ullAvailVirtual;
public ulong ullAvailExtendedVirtual;
public MEMORYSTATUSEX()
{
dwLength = (uint)Marshal.SizeOf(typeof(MEMORYSTATUSEX));
}
}
}
}

View File

@ -0,0 +1,28 @@
using System.Windows.Controls;
using System.Windows.Media;
namespace DarkBlendTheme
{
public static class TreeViewItemExtensions
{
public static int GetDepth(this TreeViewItem item)
{
TreeViewItem parent;
while ((parent = GetParent(item)) != null) return GetDepth(parent) + 1;
return 0;
}
private static TreeViewItem GetParent(TreeViewItem item)
{
var parent = VisualTreeHelper.GetParent(item);
while (!(parent is TreeViewItem || parent is TreeView))
{
if (parent == null) return null;
parent = VisualTreeHelper.GetParent(parent);
}
return parent as TreeViewItem;
}
}
}

View File

@ -0,0 +1,176 @@
using DynamicData;
using DynamicData.Binding;
using Microsoft.WindowsAPICodePack.Dialogs;
using ReactiveUI;
using System;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Reactive.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Windows.Media.Imaging;
using Wabbajack.Common;
using Wabbajack.Hashing.xxHash64;
using Wabbajack.Extensions;
using Wabbajack.Models;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
namespace Wabbajack
{
public static class UIUtils
{
public static BitmapImage BitmapImageFromResource(string name) => BitmapImageFromStream(System.Windows.Application.GetResourceStream(new Uri("pack://application:,,,/Wabbajack;component/" + name)).Stream);
public static BitmapImage BitmapImageFromStream(Stream stream)
{
var img = new BitmapImage();
img.BeginInit();
img.CacheOption = BitmapCacheOption.OnLoad;
img.StreamSource = stream;
img.EndInit();
img.Freeze();
return img;
}
public static bool TryGetBitmapImageFromFile(AbsolutePath path, out BitmapImage bitmapImage)
{
try
{
if (!path.FileExists())
{
bitmapImage = default;
return false;
}
bitmapImage = new BitmapImage(new Uri(path.ToString(), UriKind.RelativeOrAbsolute));
return true;
}
catch (Exception)
{
bitmapImage = default;
return false;
}
}
public static void OpenWebsite(Uri url)
{
Process.Start(new ProcessStartInfo("cmd.exe", $"/c start {url}")
{
CreateNoWindow = true,
});
}
public static void OpenFolder(AbsolutePath path)
{
Process.Start(new ProcessStartInfo(KnownFolders.Windows.Combine("explorer.exe").ToString(), path.ToString())
{
CreateNoWindow = true,
});
}
public static AbsolutePath OpenFileDialog(string filter, string initialDirectory = null)
{
OpenFileDialog ofd = new OpenFileDialog();
ofd.Filter = filter;
ofd.InitialDirectory = initialDirectory;
if (ofd.ShowDialog() == DialogResult.OK)
return (AbsolutePath)ofd.FileName;
return default;
}
public static IObservable<BitmapImage> DownloadBitmapImage(this IObservable<string> obs, Action<Exception> exceptionHandler,
LoadingLock loadingLock)
{
return obs
.ObserveOn(RxApp.TaskpoolScheduler)
.SelectTask(async url =>
{
var ll = loadingLock.WithLoading();
try
{
var (found, mstream) = await FindCachedImage(url);
if (found) return (ll, mstream);
var ret = new MemoryStream();
using (var client = new HttpClient())
await using (var stream = await client.GetStreamAsync(url))
{
await stream.CopyToAsync(ret);
}
ret.Seek(0, SeekOrigin.Begin);
await WriteCachedImage(url, ret.ToArray());
return (ll, ret);
}
catch (Exception ex)
{
exceptionHandler(ex);
return (ll, default);
}
})
.Select(x =>
{
var (ll, memStream) = x;
if (memStream == null) return default;
try
{
return BitmapImageFromStream(memStream);
}
catch (Exception ex)
{
exceptionHandler(ex);
return default;
}
finally
{
ll.Dispose();
memStream.Dispose();
}
})
.ObserveOnGuiThread();
}
private static async Task WriteCachedImage(string url, byte[] data)
{
var folder = KnownFolders.WabbajackAppLocal.Combine("ModListImages");
if (!folder.DirectoryExists()) folder.CreateDirectory();
var path = folder.Combine((await Encoding.UTF8.GetBytes(url).Hash()).ToHex());
await path.WriteAllBytesAsync(data);
}
private static async Task<(bool Found, MemoryStream data)> FindCachedImage(string uri)
{
var folder = KnownFolders.WabbajackAppLocal.Combine("ModListImages");
if (!folder.DirectoryExists()) folder.CreateDirectory();
var path = folder.Combine((await Encoding.UTF8.GetBytes(uri).Hash()).ToHex());
return path.FileExists() ? (true, new MemoryStream(await path.ReadAllBytesAsync())) : (false, default);
}
/// <summary>
/// Format bytes to a greater unit
/// </summary>
/// <param name="bytes">number of bytes</param>
/// <returns></returns>
public static string FormatBytes(long bytes)
{
string[] Suffix = { "B", "KB", "MB", "GB", "TB" };
int i;
double dblSByte = bytes;
for (i = 0; i < Suffix.Length && bytes >= 1024; i++, bytes /= 1024)
{
dblSByte = bytes / 1024.0;
}
return String.Format("{0:0.##} {1}", dblSByte, Suffix[i]);
}
}
}

View File

@ -0,0 +1,75 @@
using System;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Microsoft.Extensions.Logging;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.Common;
using Wabbajack;
using Wabbajack.Messages;
namespace Wabbajack
{
public interface IBackNavigatingVM : IReactiveObject
{
ViewModel NavigateBackTarget { get; set; }
ReactiveCommand<Unit, Unit> BackCommand { get; }
Subject<bool> IsBackEnabledSubject { get; }
IObservable<bool> IsBackEnabled { get; }
}
public class BackNavigatingVM : ViewModel, IBackNavigatingVM
{
[Reactive]
public ViewModel NavigateBackTarget { get; set; }
public ReactiveCommand<Unit, Unit> BackCommand { get; protected set; }
[Reactive]
public bool IsActive { get; set; }
public Subject<bool> IsBackEnabledSubject { get; } = new Subject<bool>();
public IObservable<bool> IsBackEnabled { get; }
public BackNavigatingVM(ILogger logger)
{
IsBackEnabled = IsBackEnabledSubject.StartWith(true);
BackCommand = ReactiveCommand.Create(
execute: () => logger.CatchAndLog(() =>
{
NavigateBack.Send();
Unload();
}),
canExecute: this.ConstructCanNavigateBack()
.ObserveOnGuiThread());
this.WhenActivated(disposables =>
{
IsActive = true;
Disposable.Create(() => IsActive = false).DisposeWith(disposables);
});
}
public virtual void Unload()
{
}
}
public static class IBackNavigatingVMExt
{
public static IObservable<bool> ConstructCanNavigateBack(this IBackNavigatingVM vm)
{
return vm.WhenAny(x => x.NavigateBackTarget)
.CombineLatest(vm.IsBackEnabled)
.Select(x => x.First != null && x.Second);
}
public static IObservable<bool> ConstructIsActive(this IBackNavigatingVM vm, MainWindowVM mwvm)
{
return mwvm.WhenAny(x => x.ActivePane)
.Select(x => object.ReferenceEquals(vm, x));
}
}
}

View File

@ -0,0 +1,25 @@
using System;
using ReactiveUI.Fody.Helpers;
using Wabbajack;
using Wabbajack.RateLimiter;
namespace Wabbajack
{
public class CPUDisplayVM : ViewModel
{
[Reactive]
public ulong ID { get; set; }
[Reactive]
public DateTime StartTime { get; set; }
[Reactive]
public bool IsWorking { get; set; }
[Reactive]
public string Msg { get; set; }
[Reactive]
public Percent ProgressPercent { get; set; }
public CPUDisplayVM()
{
}
}
}

View File

@ -0,0 +1,310 @@
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Reactive;
using System.Windows.Media.Imaging;
using Microsoft.Extensions.Logging;
using Wabbajack.Extensions;
using Wabbajack.Interventions;
using Wabbajack.Messages;
using Wabbajack.RateLimiter;
using ReactiveUI;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Media;
using DynamicData;
using DynamicData.Binding;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.WindowsAPICodePack.Dialogs;
using ReactiveUI.Fody.Helpers;
using Wabbajack.Common;
using Wabbajack.Compiler;
using Wabbajack.Downloaders;
using Wabbajack.Downloaders.GameFile;
using Wabbajack.DTOs;
using Wabbajack.DTOs.Interventions;
using Wabbajack.DTOs.JsonConverters;
using Wabbajack.Installer;
using Wabbajack.Models;
using Wabbajack.Networking.WabbajackClientApi;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
using Wabbajack.Services.OSIntegrated;
using Wabbajack.VFS;
namespace Wabbajack
{
public enum CompilerState
{
Configuration,
Compiling,
Completed,
Errored
}
public class CompilerVM : BackNavigatingVM, ICpuStatusVM
{
private const string LastSavedCompilerSettings = "last-saved-compiler-settings";
private readonly DTOSerializer _dtos;
private readonly SettingsManager _settingsManager;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<CompilerVM> _logger;
private readonly ResourceMonitor _resourceMonitor;
[Reactive]
public CompilerState State { get; set; }
[Reactive]
public ISubCompilerVM SubCompilerVM { get; set; }
// Paths
public FilePickerVM ModlistLocation { get; }
public FilePickerVM DownloadLocation { get; }
public FilePickerVM OutputLocation { get; }
// Modlist Settings
[Reactive] public string ModListName { get; set; }
[Reactive] public string Version { get; set; }
[Reactive] public string Author { get; set; }
[Reactive] public string Description { get; set; }
public FilePickerVM ModListImagePath { get; } = new();
[Reactive] public ImageSource ModListImage { get; set; }
[Reactive] public string Website { get; set; }
[Reactive] public string Readme { get; set; }
[Reactive] public bool IsNSFW { get; set; }
[Reactive] public bool PublishUpdate { get; set; }
[Reactive] public string MachineUrl { get; set; }
[Reactive] public Game BaseGame { get; set; }
[Reactive] public string SelectedProfile { get; set; }
[Reactive] public AbsolutePath GamePath { get; set; }
[Reactive] public bool IsMO2Compilation { get; set; }
[Reactive] public RelativePath[] AlwaysEnabled { get; set; } = Array.Empty<RelativePath>();
[Reactive] public string[] OtherProfiles { get; set; } = Array.Empty<string>();
[Reactive] public AbsolutePath Source { get; set; }
public AbsolutePath SettingsOutputLocation => Source.Combine(ModListName).WithExtension(Ext.CompilerSettings);
public ReactiveCommand<Unit, Unit> ExecuteCommand { get; }
public LoggerProvider LoggerProvider { get; }
public ReadOnlyObservableCollection<CPUDisplayVM> StatusList => _resourceMonitor.Tasks;
public CompilerVM(ILogger<CompilerVM> logger, DTOSerializer dtos, SettingsManager settingsManager,
IServiceProvider serviceProvider, LoggerProvider loggerProvider, ResourceMonitor resourceMonitor) : base(logger)
{
_logger = logger;
_dtos = dtos;
_settingsManager = settingsManager;
_serviceProvider = serviceProvider;
LoggerProvider = loggerProvider;
_resourceMonitor = resourceMonitor;
BackCommand =
ReactiveCommand.CreateFromTask(async () =>
{
await SaveSettingsFile();
NavigateToGlobal.Send(NavigateToGlobal.ScreenType.ModeSelectionView);
});
SubCompilerVM = new MO2CompilerVM(this);
ExecuteCommand = ReactiveCommand.CreateFromTask(async () => await StartCompilation());
ModlistLocation = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.CheckOptions.On,
PathType = FilePickerVM.PathTypeOptions.File,
PromptTitle = "Select a config file or a modlist.txt file"
};
DownloadLocation = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.CheckOptions.On,
PathType = FilePickerVM.PathTypeOptions.Folder,
PromptTitle = "Location where the downloads for this list are stored"
};
OutputLocation = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.CheckOptions.On,
PathType = FilePickerVM.PathTypeOptions.Folder,
PromptTitle = "Location where the compiled modlist will be stored"
};
ModlistLocation.Filters.AddRange(new []
{
new CommonFileDialogFilter("MO2 Modlist", "*" + Ext.Txt),
new CommonFileDialogFilter("Compiler Settings File", "*" + Ext.CompilerSettings)
});
this.WhenActivated(disposables =>
{
State = CompilerState.Configuration;
Disposable.Empty.DisposeWith(disposables);
ModlistLocation.WhenAnyValue(vm => vm.TargetPath)
.Subscribe(p => InferModListFromLocation(p).FireAndForget())
.DisposeWith(disposables);
LoadLastSavedSettings().FireAndForget();
});
}
private async Task InferModListFromLocation(AbsolutePath settingsFile)
{
if (settingsFile == default) return;
using var ll = LoadingLock.WithLoading();
if (settingsFile.FileName == "modlist.txt".ToRelativePath() && settingsFile.Depth > 3)
{
var mo2Folder = settingsFile.Parent.Parent.Parent;
var mo2Ini = mo2Folder.Combine(Consts.MO2IniName);
if (mo2Ini.FileExists())
{
var iniData = mo2Ini.LoadIniFile();
var general = iniData["General"];
BaseGame = GameRegistry.GetByFuzzyName(general["gameName"].FromMO2Ini()).Game;
Source = mo2Folder;
SelectedProfile = general["selected_profile"].FromMO2Ini();
GamePath = general["gamePath"].FromMO2Ini().ToAbsolutePath();
ModListName = SelectedProfile;
var settings = iniData["Settings"];
var downloadLocation = settings["download_directory"].FromMO2Ini().ToAbsolutePath();
if (downloadLocation == default)
downloadLocation = Source.Combine("downloads");
DownloadLocation.TargetPath = downloadLocation;
IsMO2Compilation = true;
AlwaysEnabled = Array.Empty<RelativePath>();
// Find Always Enabled mods
foreach (var modFolder in mo2Folder.Combine("mods").EnumerateDirectories())
{
var iniFile = modFolder.Combine("meta.ini");
if (!iniFile.FileExists()) continue;
var data = iniFile.LoadIniFile();
var generalModData = data["General"];
if ((generalModData["notes"]?.Contains("WABBAJACK_ALWAYS_ENABLE") ?? false) ||
(generalModData["comments"]?.Contains("WABBAJACK_ALWAYS_ENABLE") ?? false))
AlwaysEnabled = AlwaysEnabled.Append(modFolder.RelativeTo(mo2Folder)).ToArray();
}
var otherProfilesFile = settingsFile.Parent.Combine("otherprofiles.txt");
if (otherProfilesFile.FileExists())
{
OtherProfiles = await otherProfilesFile.ReadAllLinesAsync().ToArray();
}
if (mo2Folder.Depth > 1)
OutputLocation.TargetPath = mo2Folder.Parent;
await SaveSettingsFile();
ModlistLocation.TargetPath = SettingsOutputLocation;
}
}
}
private async Task StartCompilation()
{
var tsk = Task.Run(async () =>
{
try
{
State = CompilerState.Compiling;
var mo2Settings = new MO2CompilerSettings
{
Game = BaseGame,
ModListName = ModListName,
ModListAuthor = Author,
ModlistReadme = Readme,
Source = Source,
Downloads = DownloadLocation.TargetPath,
OutputFile = OutputLocation.TargetPath,
Profile = SelectedProfile,
OtherProfiles = OtherProfiles,
AlwaysEnabled = AlwaysEnabled
};
var compiler = new MO2Compiler(_serviceProvider.GetRequiredService<ILogger<MO2Compiler>>(),
_serviceProvider.GetRequiredService<FileExtractor.FileExtractor>(),
_serviceProvider.GetRequiredService<FileHashCache>(),
_serviceProvider.GetRequiredService<Context>(),
_serviceProvider.GetRequiredService<TemporaryFileManager>(),
mo2Settings,
_serviceProvider.GetRequiredService<ParallelOptions>(),
_serviceProvider.GetRequiredService<DownloadDispatcher>(),
_serviceProvider.GetRequiredService<Client>(),
_serviceProvider.GetRequiredService<IGameLocator>(),
_serviceProvider.GetRequiredService<DTOSerializer>(),
_serviceProvider.GetRequiredService<IResource<ACompiler>>(),
_serviceProvider.GetRequiredService<IBinaryPatchCache>());
await compiler.Begin(CancellationToken.None);
State = CompilerState.Completed;
}
catch (Exception ex)
{
State = CompilerState.Errored;
_logger.LogInformation(ex, "Failed Compilation : {Message}", ex.Message);
}
});
await tsk;
}
private async Task SaveSettingsFile()
{
if (Source == default) return;
await using var st = SettingsOutputLocation.Open(FileMode.Create, FileAccess.Write, FileShare.None);
await JsonSerializer.SerializeAsync(st, GetSettings(), _dtos.Options);
await _settingsManager.Save(LastSavedCompilerSettings, Source);
}
private async Task LoadLastSavedSettings()
{
var lastPath = await _settingsManager.Load<AbsolutePath>(LastSavedCompilerSettings);
if (Source == default) return;
Source = lastPath;
}
private CompilerSettings GetSettings()
{
return new CompilerSettings
{
ModListName = ModListName,
ModListAuthor = Author,
Downloads = DownloadLocation.TargetPath,
Source = ModlistLocation.TargetPath,
Game = BaseGame,
Profile = SelectedProfile,
UseGamePaths = true,
OutputFile = OutputLocation.TargetPath.Combine(SelectedProfile).WithExtension(Ext.Wabbajack),
AlwaysEnabled = AlwaysEnabled.ToArray(),
OtherProfiles = OtherProfiles.ToArray()
};
}
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.Threading.Tasks;
using Wabbajack.Compiler;
using Wabbajack.DTOs;
namespace Wabbajack
{
public interface ISubCompilerVM
{
ACompiler ActiveCompilation { get; }
ModlistSettingsEditorVM ModlistSettings { get; }
void Unload();
IObservable<bool> CanCompile { get; }
Task<GetResponse<ModList>> Compile();
}
}

View File

@ -0,0 +1,62 @@
using Microsoft.WindowsAPICodePack.Dialogs;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading.Tasks;
using DynamicData;
using Wabbajack.Common;
using Wabbajack.Compiler;
using Wabbajack.DTOs;
using Wabbajack.DTOs.GitHub;
using Wabbajack;
using Wabbajack.Extensions;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
using Consts = Wabbajack.Consts;
namespace Wabbajack
{
public class MO2CompilerVM : ViewModel, ISubCompilerVM
{
public CompilerVM Parent { get; }
private readonly MO2CompilationSettings _settings;
private readonly ObservableAsPropertyHelper<AbsolutePath> _mo2Folder;
public AbsolutePath Mo2Folder => _mo2Folder.Value;
private readonly ObservableAsPropertyHelper<string> _moProfile;
public string MOProfile => _moProfile.Value;
public FilePickerVM DownloadLocation { get; }
public FilePickerVM ModListLocation { get; }
[Reactive]
public ACompiler ActiveCompilation { get; private set; }
private readonly ObservableAsPropertyHelper<ModlistSettingsEditorVM> _modlistSettings;
private readonly IObservable<IChangeSet<string>> _authorKeys;
public ModlistSettingsEditorVM ModlistSettings => _modlistSettings.Value;
[Reactive]
public object StatusTracker { get; private set; }
public void Unload()
{
throw new NotImplementedException();
}
public IObservable<bool> CanCompile { get; }
public Task<GetResponse<ModList>> Compile()
{
throw new NotImplementedException();
}
public MO2CompilerVM(CompilerVM parent)
{
}
}
}

View File

@ -0,0 +1,109 @@
using System;
using System.Reactive.Linq;
using System.Windows.Input;
using DynamicData;
using Microsoft.WindowsAPICodePack.Dialogs;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.Common;
using Wabbajack;
namespace Wabbajack
{
public class ModlistSettingsEditorVM : ViewModel
{
private readonly CompilationModlistSettings _settings;
[Reactive]
public string ModListName { get; set; }
[Reactive]
public string VersionText { get; set; }
private readonly ObservableAsPropertyHelper<Version> _version;
public Version Version => _version.Value;
[Reactive]
public string AuthorText { get; set; }
[Reactive]
public string Description { get; set; }
public FilePickerVM ImagePath { get; }
[Reactive]
public string Readme { get; set; }
[Reactive] public string MachineUrl { get; set; } = "";
[Reactive] public bool Publish { get; set; } = false;
[Reactive]
public string Website { get; set; }
[Reactive]
public bool IsNSFW { get; set; }
public IObservable<bool> InError { get; }
public ModlistSettingsEditorVM(CompilationModlistSettings settings)
{
_settings = settings;
ImagePath = new FilePickerVM
{
ExistCheckOption = FilePickerVM.CheckOptions.IfPathNotEmpty,
PathType = FilePickerVM.PathTypeOptions.File,
};
ImagePath.Filters.Add(new CommonFileDialogFilter("Banner image", "*.png"));
_version = this.WhenAny(x => x.VersionText)
.Select(x =>
{
if (string.IsNullOrWhiteSpace(x))
return new Version(0, 0);
return !Version.TryParse(x, out var version) ? new Version(0, 0) : version;
}).ObserveOnGuiThread()
.ToProperty(this, x => x.Version);
InError = this.WhenAny(x => x.ImagePath.ErrorState)
.Select(err => err.Failed)
.CombineLatest(
this.WhenAny(x => x.VersionText)
.Select(x => Version.TryParse(x, out _)),
(image, version) => !image && !version)
.Publish()
.RefCount();
}
public void Init()
{
AuthorText = _settings.Author;
if (!string.IsNullOrWhiteSpace(_settings.ModListName))
{
ModListName = _settings.ModListName;
}
Description = _settings.Description;
Readme = _settings.Readme;
ImagePath.TargetPath = _settings.SplashScreen;
Website = _settings.Website;
VersionText = _settings.Version;
IsNSFW = _settings.IsNSFW;
MachineUrl = _settings.MachineUrl;
Publish = _settings.Publish;
}
public void Save()
{
_settings.Version = VersionText;
_settings.Author = AuthorText;
_settings.ModListName = ModListName;
_settings.Description = Description;
_settings.Readme = Readme;
_settings.SplashScreen = ImagePath.TargetPath;
_settings.Website = Website;
_settings.IsNSFW = IsNSFW;
_settings.MachineUrl = MachineUrl;
_settings.Publish = Publish;
}
}
}

View File

@ -0,0 +1,220 @@

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using DynamicData;
using Microsoft.Extensions.Logging;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.Common;
using Wabbajack.Downloaders.GameFile;
using Wabbajack.DTOs;
using Wabbajack.Messages;
using Wabbajack.Networking.WabbajackClientApi;
using Wabbajack.Services.OSIntegrated;
using Wabbajack.Services.OSIntegrated.Services;
namespace Wabbajack
{
public class ModListGalleryVM : BackNavigatingVM
{
public MainWindowVM MWVM { get; }
private readonly SourceCache<ModListMetadataVM, string> _modLists = new(x => x.Metadata.Links.MachineURL);
public ReadOnlyObservableCollection<ModListMetadataVM> _filteredModLists;
public ReadOnlyObservableCollection<ModListMetadataVM> ModLists => _filteredModLists;
private const string ALL_GAME_TYPE = "All";
[Reactive] public IErrorResponse Error { get; set; }
[Reactive] public string Search { get; set; }
[Reactive] public bool OnlyInstalled { get; set; }
[Reactive] public bool ShowNSFW { get; set; }
[Reactive] public bool ShowUtilityLists { get; set; }
[Reactive] public string GameType { get; set; }
public List<string> GameTypeEntries => GetGameTypeEntries();
private ObservableAsPropertyHelper<bool> _Loaded;
private readonly Client _wjClient;
private readonly ILogger<ModListGalleryVM> _logger;
private readonly GameLocator _locator;
private readonly ModListDownloadMaintainer _maintainer;
private readonly SettingsManager _settingsManager;
private FiltersSettings settings { get; set; } = new();
public ICommand ClearFiltersCommand { get; set; }
public ModListGalleryVM(ILogger<ModListGalleryVM> logger, Client wjClient,
GameLocator locator, SettingsManager settingsManager, ModListDownloadMaintainer maintainer)
: base(logger)
{
_wjClient = wjClient;
_logger = logger;
_locator = locator;
_maintainer = maintainer;
_settingsManager = settingsManager;
ClearFiltersCommand = ReactiveCommand.Create(
() =>
{
OnlyInstalled = false;
ShowNSFW = false;
ShowUtilityLists = false;
Search = string.Empty;
GameType = ALL_GAME_TYPE;
});
BackCommand = ReactiveCommand.Create(
() =>
{
NavigateToGlobal.Send(NavigateToGlobal.ScreenType.ModeSelectionView);
});
this.WhenActivated(disposables =>
{
LoadModLists().FireAndForget();
LoadSettings().FireAndForget();
Disposable.Create(() => SaveSettings().FireAndForget())
.DisposeWith(disposables);
var searchTextPredicates = this.ObservableForProperty(vm => vm.Search)
.Select(change => change.Value)
.StartWith("")
.Select<string, Func<ModListMetadataVM, bool>>(txt =>
{
if (string.IsNullOrWhiteSpace(txt)) return _ => true;
return item => item.Metadata.Title.ContainsCaseInsensitive(txt) ||
item.Metadata.Description.ContainsCaseInsensitive(txt);
});
var onlyInstalledGamesFilter = this.ObservableForProperty(vm => vm.OnlyInstalled)
.Select(v => v.Value)
.Select<bool, Func<ModListMetadataVM, bool>>(onlyInstalled =>
{
if (onlyInstalled == false) return _ => true;
return item => _locator.IsInstalled(item.Metadata.Game);
})
.StartWith(_ => true);
var onlyUtilityListsFilter = this.ObservableForProperty(vm => vm.ShowUtilityLists)
.Select(v => v.Value)
.Select<bool, Func<ModListMetadataVM, bool>>(utility =>
{
if (utility == false) return item => item.Metadata.UtilityList == false;
return item => item.Metadata.UtilityList;
})
.StartWith(item => item.Metadata.UtilityList == false);
var showNSFWFilter = this.ObservableForProperty(vm => vm.ShowNSFW)
.Select(v => v.Value)
.Select<bool, Func<ModListMetadataVM, bool>>(showNsfw => { return item => item.Metadata.NSFW == showNsfw; })
.StartWith(item => item.Metadata.NSFW == false);
var gameFilter = this.ObservableForProperty(vm => vm.GameType)
.Select(v => v.Value)
.Select<string, Func<ModListMetadataVM, bool>>(selected =>
{
if (selected is null or ALL_GAME_TYPE) return _ => true;
return item => item.Metadata.Game.MetaData().HumanFriendlyGameName == selected;
})
.StartWith(_ => true);
_modLists.Connect()
.ObserveOn(RxApp.MainThreadScheduler)
.Filter(searchTextPredicates)
.Filter(onlyInstalledGamesFilter)
.Filter(onlyUtilityListsFilter)
.Filter(showNSFWFilter)
.Filter(gameFilter)
.Bind(out _filteredModLists)
.Subscribe()
.DisposeWith(disposables);
});
}
private class FilterSettings
{
public string GameType { get; set; }
public bool ShowNSFW { get; set; }
public bool ShowUtilityLists { get; set; }
public bool OnlyInstalled { get; set; }
public string Search { get; set; }
}
public override void Unload()
{
Error = null;
}
private async Task SaveSettings()
{
await _settingsManager.Save("modlist_gallery", new FilterSettings
{
GameType = GameType,
ShowNSFW = ShowNSFW,
ShowUtilityLists = ShowUtilityLists,
Search = Search,
OnlyInstalled = OnlyInstalled,
});
}
private async Task LoadSettings()
{
using var ll = LoadingLock.WithLoading();
RxApp.MainThreadScheduler.Schedule(await _settingsManager.Load<FilterSettings>("modlist_gallery"),
(_, s) =>
{
GameType = s.GameType;
ShowNSFW = s.ShowNSFW;
ShowUtilityLists = s.ShowUtilityLists;
Search = s.Search;
OnlyInstalled = s.OnlyInstalled;
return Disposable.Empty;
});
}
private async Task LoadModLists()
{
using var ll = LoadingLock.WithLoading();
try
{
var modLists = await _wjClient.LoadLists();
_modLists.Edit(e =>
{
e.Clear();
e.AddOrUpdate(modLists.Select(m =>
new ModListMetadataVM(_logger, this, m, _maintainer, _wjClient)));
});
}
catch (Exception ex)
{
_logger.LogError(ex, "While loading lists");
ll.Fail();
}
ll.Succeed();
}
private List<string> GetGameTypeEntries()
{
List<string> gameEntries = new List<string> {ALL_GAME_TYPE};
gameEntries.AddRange(GameRegistry.Games.Values.Select(gameType => gameType.HumanFriendlyGameName));
gameEntries.Sort();
return gameEntries;
}
}
}

View File

@ -0,0 +1,223 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Threading.Tasks;
using System.Windows.Input;
using System.Windows.Media.Imaging;
using DynamicData;
using Microsoft.Extensions.Logging;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.Common;
using Wabbajack.DTOs;
using Wabbajack.DTOs.ServerResponses;
using Wabbajack;
using Wabbajack.Extensions;
using Wabbajack.Messages;
using Wabbajack.Models;
using Wabbajack.Networking.WabbajackClientApi;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
using Wabbajack.RateLimiter;
using Wabbajack.Services.OSIntegrated.Services;
namespace Wabbajack
{
public struct ModListTag
{
public ModListTag(string name)
{
Name = name;
}
public string Name { get; }
}
public class ModListMetadataVM : ViewModel
{
public ModlistMetadata Metadata { get; }
private ModListGalleryVM _parent;
public ICommand OpenWebsiteCommand { get; }
public ICommand ExecuteCommand { get; }
public ICommand ModListContentsCommend { get; }
private readonly ObservableAsPropertyHelper<bool> _Exists;
public bool Exists => _Exists.Value;
public AbsolutePath Location { get; }
public LoadingLock LoadingImageLock { get; } = new();
[Reactive]
public List<ModListTag> ModListTagList { get; private set; }
[Reactive]
public Percent ProgressPercent { get; private set; }
[Reactive]
public bool IsBroken { get; private set; }
[Reactive]
public ModListStatus Status { get; set; }
[Reactive]
public bool IsDownloading { get; private set; }
[Reactive]
public string DownloadSizeText { get; private set; }
[Reactive]
public string InstallSizeText { get; private set; }
[Reactive]
public string VersionText { get; private set; }
[Reactive]
public IErrorResponse Error { get; private set; }
private readonly ObservableAsPropertyHelper<BitmapImage> _Image;
public BitmapImage Image => _Image.Value;
private readonly ObservableAsPropertyHelper<bool> _LoadingImage;
public bool LoadingImage => _LoadingImage.Value;
private Subject<bool> IsLoadingIdle;
private readonly ILogger _logger;
private readonly ModListDownloadMaintainer _maintainer;
private readonly Client _wjClient;
public ModListMetadataVM(ILogger logger, ModListGalleryVM parent, ModlistMetadata metadata,
ModListDownloadMaintainer maintainer, Client wjClient)
{
_logger = logger;
_parent = parent;
_maintainer = maintainer;
Metadata = metadata;
_wjClient = wjClient;
Location = LauncherUpdater.CommonFolder.Value.Combine("downloaded_mod_lists", Metadata.Links.MachineURL).WithExtension(Ext.Wabbajack);
ModListTagList = new List<ModListTag>();
UpdateStatus().FireAndForget();
Metadata.Tags.ForEach(tag =>
{
ModListTagList.Add(new ModListTag(tag));
});
ModListTagList.Add(new ModListTag(metadata.Game.MetaData().HumanFriendlyGameName));
DownloadSizeText = "Download size : " + UIUtils.FormatBytes(Metadata.DownloadMetadata.SizeOfArchives);
InstallSizeText = "Installation size : " + UIUtils.FormatBytes(Metadata.DownloadMetadata.SizeOfInstalledFiles);
VersionText = "Modlist version : " + Metadata.Version;
IsBroken = metadata.ValidationSummary.HasFailures || metadata.ForceDown;
//https://www.wabbajack.org/#/modlists/info?machineURL=eldersouls
OpenWebsiteCommand = ReactiveCommand.Create(() => UIUtils.OpenWebsite(new Uri($"https://www.wabbajack.org/#/modlists/info?machineURL={Metadata.Links.MachineURL}")));
IsLoadingIdle = new Subject<bool>();
ModListContentsCommend = ReactiveCommand.Create(async () =>
{
_parent.MWVM.ModListContentsVM.Value.Name = metadata.Title;
IsLoadingIdle.OnNext(false);
try
{
var status = await wjClient.GetDetailedStatus(metadata.Links.MachineURL);
var coll = _parent.MWVM.ModListContentsVM.Value.Status;
coll.Clear();
coll.AddRange(status.Archives.Select(a => new DetailedStatusItem
{
Archive = a.Original,
ArchiveStatus = a.Status,
IsFailing = a.Status != ArchiveStatus.InValid
}));
NavigateToGlobal.Send(NavigateToGlobal.ScreenType.ModListContents);
}
finally
{
IsLoadingIdle.OnNext(true);
}
}, IsLoadingIdle.StartWith(true));
ExecuteCommand = ReactiveCommand.CreateFromTask(async () =>
{
if (await _maintainer.HaveModList(Metadata))
{
LoadModlistForInstalling.Send(_maintainer.ModListPath(Metadata), Metadata);
NavigateToGlobal.Send(NavigateToGlobal.ScreenType.Installer);
}
else
{
await Download();
}
}, LoadingLock.WhenAnyValue(ll => ll.IsLoading).Select(v => !v));
_Exists = Observable.Interval(TimeSpan.FromSeconds(0.5))
.Unit()
.StartWith(Unit.Default)
.FlowSwitch(_parent.WhenAny(x => x.IsActive))
.SelectAsync(async _ =>
{
try
{
return !IsDownloading && await maintainer.HaveModList(metadata);
}
catch (Exception)
{
return true;
}
})
.ToGuiProperty(this, nameof(Exists));
var imageObs = Observable.Return(Metadata.Links.ImageUri)
.DownloadBitmapImage((ex) => _logger.LogError("Error downloading modlist image {Title}", Metadata.Title), LoadingImageLock);
_Image = imageObs
.ToGuiProperty(this, nameof(Image));
_LoadingImage = imageObs
.Select(x => false)
.StartWith(true)
.ToGuiProperty(this, nameof(LoadingImage));
}
private async Task Download()
{
Status = ModListStatus.Downloading;
using var ll = LoadingLock.WithLoading();
var (progress, task) = _maintainer.DownloadModlist(Metadata);
var dispose = progress
.BindToStrict(this, vm => vm.ProgressPercent);
await task;
await _wjClient.SendMetric("downloading", Metadata.Title);
await UpdateStatus();
dispose.Dispose();
}
private async Task UpdateStatus()
{
if (await _maintainer.HaveModList(Metadata))
Status = ModListStatus.Downloaded;
else if (LoadingLock.IsLoading)
Status = ModListStatus.Downloading;
else
Status = ModListStatus.NotDownloaded;
}
public enum ModListStatus
{
NotDownloaded,
Downloading,
Downloaded
}
}
}

View File

@ -0,0 +1,16 @@
using Wabbajack.DTOs;
namespace Wabbajack
{
public class GameVM
{
public Game Game { get; }
public string DisplayName { get; }
public GameVM(Game game)
{
Game = game;
DisplayName = game.MetaData().HumanFriendlyGameName;
}
}
}

View File

@ -0,0 +1,19 @@
using System.Threading.Tasks;
using Wabbajack.Installer;
using Wabbajack.DTOs.Interventions;
namespace Wabbajack
{
public interface ISubInstallerVM
{
InstallerVM Parent { get; }
IInstaller ActiveInstallation { get; }
void Unload();
bool SupportsAfterInstallNavigation { get; }
void AfterInstallNavigation();
int ConfigVisualVerticalOffset { get; }
ErrorResponse CanInstall { get; }
Task<bool> Install();
IUserIntervention InterventionConverter(IUserIntervention intervention);
}
}

Some files were not shown because too many files have changed in this diff Show More