Initial import of WPF code

This commit is contained in:
Timothy Baldridge 2021-12-26 14:56:44 -07:00
parent adba503183
commit 31f32ee2f5
187 changed files with 17880 additions and 0 deletions

View File

@ -0,0 +1,18 @@
<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"
StartupUri="Views\MainWindow.xaml">
<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,26 @@
using System;
using System.Windows;
using Wabbajack.Common;
using Wabbajack.Lib;
namespace Wabbajack
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
public App()
{
Consts.LogsFolder = LauncherUpdater.CommonFolder.Value.Combine("logs");
Consts.LogsFolder.CreateDirectory();
LoggingSettings.LogToFile = true;
Utils.InitializeLogging().Wait();
CLIOld.ParseOptions(Environment.GetCommandLineArgs());
if (CLIArguments.Help)
CLIOld.DisplayHelpText();
}
}
}

View File

@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Wabbajack.Common;
using Wabbajack.Common.Serialization.Json;
using Wabbajack.Lib;
using Wabbajack.Lib.ModListRegistry;
namespace Wabbajack
{
[JsonName("DetailedStatus")]
public class DetailedStatus
{
public string Name { get; set; } = "";
public DateTime Checked { get; set; } = DateTime.UtcNow;
public List<DetailedStatusItem> Archives { get; set; } = new();
public DownloadMetadata DownloadMetaData { get; set; } = new();
public bool HasFailures { get; set; }
public string MachineName { get; set; } = "";
}
[JsonName("DetailedStatusItem")]
public class DetailedStatusItem
{
public bool IsFailing { get; set; }
public Archive Archive { get; set; }
public string Name => string.IsNullOrWhiteSpace(Archive!.Name) ? Archive.State.PrimaryKeyString : Archive.Name;
public string Url => Archive?.State.GetManifestURL(Archive!);
[JsonIgnore]
public bool HasUrl => Url != null;
public ArchiveStatus ArchiveStatus { get; set; }
}
public enum ArchiveStatus
{
Valid,
InValid,
Updating,
Updated,
Mirrored
}
public class ClientAPIEx
{
public static async Task<DetailedStatus> GetDetailedStatus(string machineURL)
{
var client = await ClientAPI.GetClient();
var results =
await client.GetJsonAsync<DetailedStatus>(
$"{Consts.WabbajackBuildServerUri}lists/status/{machineURL}.json");
return results;
}
}
}

View File

@ -0,0 +1,79 @@
using System;
using System.Globalization;
using System.Windows.Data;
using ReactiveUI;
using Wabbajack.Common;
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 = (string)abs;
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,80 @@
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;
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,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,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,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,197 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reactive;
using System.Reactive.Subjects;
using System.Threading.Tasks;
using Wabbajack.Common;
using Wabbajack.Common.Serialization.Json;
using Wabbajack.Lib;
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("CompilerSettings")]
public class CompilerSettings
{
public ModManager LastCompiledModManager { get; set; }
public AbsolutePath OutputLocation { get; set; }
public MO2CompilationSettings MO2Compilation { get; } = new MO2CompilationSettings();
}
[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,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,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.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Windows.Input;
using Wabbajack.Common;
using Wabbajack.Lib;
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.Exists;
case PathTypeOptions.File:
return t.Path.IsFile;
case PathTypeOptions.Folder:
return t.Path.IsDirectory;
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.Exists ? TargetPath.Parent : TargetPath;
var dlg = new CommonOpenFileDialog
{
Title = PromptTitle,
IsFolderPicker = PathType == PathTypeOptions.Folder,
InitialDirectory = (string)dirPath,
AddToMostRecentlyUsedList = false,
AllowNonFileSystemItems = false,
DefaultDirectory = (string)dirPath,
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,117 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using PInvoke;
using Silk.NET.Core.Native;
using Silk.NET.DXGI;
using Wabbajack.Common;
using Wabbajack.Lib;
using static PInvoke.User32;
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 static class SystemParametersConstructor
{
private static 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 static 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)
{
Utils.ErrorThrow(e);
}
finally
{
if (factory1->LpVtbl != (void**)IntPtr.Zero)
factory1->Release();
}
}
var memory = Utils.GetMemoryStatus();
return new SystemParameters
{
ScreenWidth = width,
ScreenHeight = height,
VideoMemorySize = (long)dxgiMemory,
SystemMemorySize = (long)memory.ullTotalPhys,
SystemPageSize = (long)memory.ullTotalPageFile - (long)memory.ullTotalPhys
};
}
}
}

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,166 @@
using DynamicData;
using DynamicData.Binding;
using Microsoft.WindowsAPICodePack.Dialogs;
using ReactiveUI;
using System;
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;
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.Exists)
{
bitmapImage = default;
return false;
}
bitmapImage = new BitmapImage(new Uri((string)path, UriKind.RelativeOrAbsolute));
return true;
}
catch (Exception)
{
bitmapImage = default;
return false;
}
}
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 IDisposable BindCpuStatus(IObservable<CPUStatus> status, ObservableCollectionExtended<CPUDisplayVM> list)
{
return status.ObserveOn(RxApp.TaskpoolScheduler)
.ToObservableChangeSet(x => x.ID)
.Batch(TimeSpan.FromMilliseconds(50), RxApp.TaskpoolScheduler)
.EnsureUniqueChanges()
.ObserveOnGuiThread()
.TransformAndCache(
onAdded: (key, cpu) => new CPUDisplayVM(cpu),
onUpdated: (change, vm) => vm.AbsorbStatus(change.Current))
.AutoRefresh(x => x.IsWorking)
.AutoRefresh(x => x.StartTime)
.Filter(i => i.IsWorking && i.ID != WorkQueue.UnassignedCpuId)
.Sort(SortExpressionComparer<CPUDisplayVM>.Ascending(s => s.StartTime))
.Bind(list)
.Subscribe();
}
public static IObservable<BitmapImage> DownloadBitmapImage(this IObservable<string> obs, Action<Exception> exceptionHandler)
{
return obs
.ObserveOn(RxApp.TaskpoolScheduler)
.SelectTask(async url =>
{
try
{
var (found, mstream) = await FindCachedImage(url);
if (found) return 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 ret;
}
catch (Exception ex)
{
exceptionHandler(ex);
return default;
}
})
.Select(memStream =>
{
if (memStream == null) return default;
try
{
return BitmapImageFromStream(memStream);
}
catch (Exception ex)
{
exceptionHandler(ex);
return default;
}
finally
{
memStream.Dispose();
}
})
.ObserveOnGuiThread();
}
private static async Task WriteCachedImage(string url, byte[] data)
{
var folder = Consts.LocalAppDataPath.Combine("ModListImages");
if (!folder.Exists) folder.CreateDirectory();
var path = folder.Combine(Encoding.UTF8.GetBytes(url).xxHash().ToHex());
await path.WriteAllBytesAsync(data);
}
private static async Task<(bool Found, MemoryStream data)> FindCachedImage(string uri)
{
var folder = Consts.LocalAppDataPath.Combine("ModListImages");
if (!folder.Exists) folder.CreateDirectory();
var path = folder.Combine(Encoding.UTF8.GetBytes(uri).xxHash().ToHex());
return path.Exists ? (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,74 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.Common;
using Wabbajack.Lib;
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; }
protected ObservableAsPropertyHelper<bool> _IsActive;
public bool IsActive => _IsActive.Value;
public Subject<bool> IsBackEnabledSubject { get; } = new Subject<bool>();
public IObservable<bool> IsBackEnabled { get; }
public BackNavigatingVM(MainWindowVM mainWindowVM)
{
IsBackEnabled = IsBackEnabledSubject.StartWith(true);
BackCommand = ReactiveCommand.Create(
execute: () => Utils.CatchAndLog(() =>
{
mainWindowVM.NavigateTo(NavigateBackTarget);
Unload();
}),
canExecute: this.ConstructCanNavigateBack()
.ObserveOnGuiThread());
_IsActive = this.ConstructIsActive(mainWindowVM)
.ToGuiProperty(this, nameof(IsActive));
}
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,48 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ReactiveUI.Fody.Helpers;
using Wabbajack.Common;
using Wabbajack.Lib;
namespace Wabbajack
{
public class CPUDisplayVM : ViewModel
{
[Reactive]
public int 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()
{
}
public CPUDisplayVM(CPUStatus cpu)
{
AbsorbStatus(cpu);
}
public void AbsorbStatus(CPUStatus cpu)
{
bool starting = cpu.IsWorking && !IsWorking;
if (starting)
{
StartTime = DateTime.Now;
}
ID = cpu.ID;
Msg = cpu.Msg;
ProgressPercent = cpu.ProgressPercent;
IsWorking = cpu.IsWorking;
}
}
}

View File

@ -0,0 +1,272 @@
using DynamicData;
using DynamicData.Binding;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Windows.Input;
using System.Windows.Media.Imaging;
using Wabbajack.Common;
using Wabbajack.Common.StatusFeed;
using Wabbajack.Lib;
using Wabbajack.Lib.AuthorApi;
using Wabbajack.Lib.FileUploader;
namespace Wabbajack
{
public class CompilerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM
{
public MainWindowVM MWVM { get; }
private readonly ObservableAsPropertyHelper<BitmapImage> _image;
public BitmapImage Image => _image.Value;
[Reactive]
public ModManager SelectedCompilerType { get; set; }
private readonly ObservableAsPropertyHelper<ISubCompilerVM> _compiler;
public ISubCompilerVM Compiler => _compiler.Value;
private readonly ObservableAsPropertyHelper<ModlistSettingsEditorVM> _currentModlistSettings;
public ModlistSettingsEditorVM CurrentModlistSettings => _currentModlistSettings.Value;
private readonly ObservableAsPropertyHelper<bool> _compiling;
public bool Compiling => _compiling.Value;
private readonly ObservableAsPropertyHelper<Percent> _percentCompleted;
public Percent PercentCompleted => _percentCompleted.Value;
public ObservableCollectionExtended<CPUDisplayVM> StatusList { get; } = new ObservableCollectionExtended<CPUDisplayVM>();
public ObservableCollectionExtended<IStatusMessage> Log => MWVM.Log;
public ReactiveCommand<Unit, Unit> GoToCommand { get; }
public ReactiveCommand<Unit, Unit> CloseWhenCompleteCommand { get; }
public ReactiveCommand<Unit, Unit> BeginCommand { get; }
public FilePickerVM OutputLocation { get; }
private readonly ObservableAsPropertyHelper<IUserIntervention> _ActiveGlobalUserIntervention;
public IUserIntervention ActiveGlobalUserIntervention => _ActiveGlobalUserIntervention.Value;
[Reactive]
public bool StartedCompilation { get; set; }
[Reactive]
public ErrorResponse? Completed { get; set; }
private readonly ObservableAsPropertyHelper<string> _progressTitle;
public string ProgressTitle => _progressTitle.Value;
private readonly ObservableAsPropertyHelper<(int CurrentCPUs, int DesiredCPUs)> _CurrentCpuCount;
public (int CurrentCPUs, int DesiredCPUs) CurrentCpuCount => _CurrentCpuCount.Value;
public CompilerVM(MainWindowVM mainWindowVM) : base(mainWindowVM)
{
MWVM = mainWindowVM;
OutputLocation = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.CheckOptions.IfPathNotEmpty,
PathType = FilePickerVM.PathTypeOptions.Folder,
PromptTitle = "Select the folder to export the compiled Wabbajack ModList to",
};
// Load settings
CompilerSettings settings = MWVM.Settings.Compiler;
SelectedCompilerType = settings.LastCompiledModManager;
OutputLocation.TargetPath = settings.OutputLocation;
MWVM.Settings.SaveSignal
.Subscribe(_ =>
{
settings.LastCompiledModManager = SelectedCompilerType;
settings.OutputLocation = OutputLocation.TargetPath;
})
.DisposeWith(CompositeDisposable);
// Swap to proper sub VM based on selected type
_compiler = this.WhenAny(x => x.SelectedCompilerType)
// Delay so the initial VM swap comes in immediately, subVM comes right after
.DelayInitial(TimeSpan.FromMilliseconds(50), RxApp.MainThreadScheduler)
.Select<ModManager, ISubCompilerVM>(type =>
{
switch (type)
{
case ModManager.MO2:
return new MO2CompilerVM(this);
default:
return null;
}
})
// Unload old VM
.Pairwise()
.Do(pair =>
{
pair.Previous?.Unload();
})
.Select(p => p.Current)
.ToGuiProperty(this, nameof(Compiler));
// Let sub VM determine what settings we're displaying and when
_currentModlistSettings = this.WhenAny(x => x.Compiler.ModlistSettings)
.ToGuiProperty(this, nameof(CurrentModlistSettings));
_image = this.WhenAny(x => x.CurrentModlistSettings.ImagePath.TargetPath)
// Throttle so that it only loads image after any sets of swaps have completed
.Throttle(TimeSpan.FromMilliseconds(50), RxApp.MainThreadScheduler)
.DistinctUntilChanged()
.ObserveOnGuiThread()
.Select(path =>
{
if (path == default) return UIUtils.BitmapImageFromResource("Resources/Wabba_Mouth_No_Text.png");
return UIUtils.TryGetBitmapImageFromFile(path, out var image) ? image : null;
})
.ToGuiProperty(this, nameof(Image));
_compiling = this.WhenAny(x => x.Compiler.ActiveCompilation)
.Select(compilation => compilation != null)
.ToGuiProperty(this, nameof(Compiling));
BackCommand = ReactiveCommand.Create(
execute: () =>
{
mainWindowVM.NavigateTo(mainWindowVM.ModeSelectionVM);
StartedCompilation = false;
Completed = null;
},
canExecute: Observable.CombineLatest(
this.WhenAny(x => x.Compiling)
.Select(x => !x),
this.ConstructCanNavigateBack(),
resultSelector: (i, b) => i && b)
.ObserveOnGuiThread());
UIUtils.BindCpuStatus(
this.WhenAny(x => x.Compiler.ActiveCompilation)
.SelectMany(c => c?.QueueStatus ?? Observable.Empty<CPUStatus>()),
StatusList)
.DisposeWith(CompositeDisposable);
_percentCompleted = this.WhenAny(x => x.Compiler.ActiveCompilation)
.StartWith(default(ACompiler))
.CombineLatest(
this.WhenAny(x => x.Completed),
(compiler, completed) =>
{
if (compiler == null)
{
return Observable.Return<Percent>(completed != null ? Percent.One : Percent.Zero);
}
return compiler.PercentCompleted.StartWith(Percent.Zero);
})
.Switch()
.Debounce(TimeSpan.FromMilliseconds(25), RxApp.MainThreadScheduler)
.ToGuiProperty(this, nameof(PercentCompleted));
BeginCommand = ReactiveCommand.CreateFromTask(
canExecute: this.WhenAny(x => x.Compiler.CanCompile)
.Switch(),
execute: async () =>
{
try
{
IsBackEnabledSubject.OnNext(false);
var modList = await this.Compiler.Compile();
Completed = ErrorResponse.Create(modList.Succeeded);
}
catch (Exception ex)
{
Completed = ErrorResponse.Fail(ex);
while (ex.InnerException != null) ex = ex.InnerException;
Utils.Error(ex, $"Compiler error");
}
finally
{
IsBackEnabledSubject.OnNext(true);
}
});
// When sub compiler begins a compile, mark state variable
BeginCommand.StartingExecution()
.Subscribe(_ =>
{
StartedCompilation = true;
})
.DisposeWith(CompositeDisposable);
// Listen for user interventions, and compile a dynamic list of all unhandled ones
var activeInterventions = this.WhenAny(x => x.Compiler.ActiveCompilation)
.SelectMany(c => c?.LogMessages ?? Observable.Empty<IStatusMessage>())
.WhereCastable<IStatusMessage, IUserIntervention>()
.ToObservableChangeSet()
.AutoRefresh(i => i.Handled)
.Filter(i => !i.Handled)
.AsObservableList();
// Find the top intervention /w no CPU ID to be marked as "global"
_ActiveGlobalUserIntervention = activeInterventions.Connect()
.Filter(x => x.CpuID == WorkQueue.UnassignedCpuId)
.QueryWhenChanged(query => query.FirstOrDefault())
.ToGuiProperty(this, nameof(ActiveGlobalUserIntervention));
CloseWhenCompleteCommand = ReactiveCommand.CreateFromTask(
canExecute: this.WhenAny(x => x.Completed)
.Select(x => x != null),
execute: async () =>
{
await MWVM.ShutdownApplication();
});
GoToCommand = ReactiveCommand.Create(
canExecute: this.WhenAny(x => x.Completed)
.Select(x => x != null),
execute: () =>
{
if (Completed?.Failed ?? false)
{
Process.Start("explorer.exe", $"/select,\"{Utils.LogFolder}\"");
}
else
{
Process.Start("explorer.exe",
OutputLocation.TargetPath == default
? $"/select,\"{Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location)}\""
: $"/select,\"{OutputLocation.TargetPath}\"");
}
});
_progressTitle = this.WhenAnyValue(
x => x.Compiling,
x => x.StartedCompilation,
x => x.Completed,
selector: (compiling, started, completed) =>
{
if (compiling)
{
return "Compiling";
}
else if (started)
{
if (completed == null) return "Compiling";
return completed.Value.Succeeded ? "Compiled" : "Failed";
}
else
{
return "Awaiting Input";
}
})
.ToGuiProperty(this, nameof(ProgressTitle));
_CurrentCpuCount = this.WhenAny(x => x.Compiler.ActiveCompilation.Queue.CurrentCpuCount)
.Switch()
.ToGuiProperty(this, nameof(CurrentCpuCount));
}
}
}

View File

@ -0,0 +1,17 @@
using System;
using System.Threading.Tasks;
using ReactiveUI;
using Wabbajack.Common;
using Wabbajack.Lib;
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,268 @@
using Microsoft.WindowsAPICodePack.Dialogs;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading.Tasks;
using DynamicData;
using Wabbajack.Common;
using Wabbajack.Lib;
using Wabbajack.Lib.AuthorApi;
using Wabbajack.Lib.FileUploader;
using Wabbajack.Lib.GitHub;
using WebSocketSharp;
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 StatusUpdateTracker StatusTracker { get; private set; }
public IObservable<bool> CanCompile { get; }
public MO2CompilerVM(CompilerVM parent)
{
Parent = parent;
ModListLocation = new FilePickerVM
{
ExistCheckOption = FilePickerVM.CheckOptions.On,
PathType = FilePickerVM.PathTypeOptions.File,
PromptTitle = "Select a Modlist"
};
ModListLocation.Filters.Add(new CommonFileDialogFilter("MO2 Profile (modlist.txt) or Native Settings (native_compiler_settings.json)", "*.txt,*.json"));
DownloadLocation = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.CheckOptions.On,
PathType = FilePickerVM.PathTypeOptions.Folder,
PromptTitle = "Select a downloads location",
};
_mo2Folder = this.WhenAny(x => x.ModListLocation.TargetPath)
.Select(loc =>
{
try
{
if (loc.FileName == Consts.ModListTxt)
{
var profileFolder = loc.Parent;
return profileFolder.Parent.Parent;
}
if (loc.FileName == Consts.NativeSettingsJson)
{
return loc.Parent;
}
return default;
}
catch (Exception)
{
return default;
}
})
.ToGuiProperty(this, nameof(Mo2Folder));
_moProfile = this.WhenAny(x => x.ModListLocation.TargetPath)
.Select(loc =>
{
try
{
if (loc.FileName == Consts.NativeSettingsJson)
{
var settings = loc.FromJson<NativeCompilerSettings>();
return settings.ModListName;
}
return (string)loc.Parent.FileName;
}
catch (Exception)
{
return null;
}
})
.ToGuiProperty(this, nameof(MOProfile));
// Wire missing Mo2Folder to signal error state for ModList Location
ModListLocation.AdditionalError = this.WhenAny(x => x.Mo2Folder)
.Select<AbsolutePath, IErrorResponse>(moFolder =>
{
if (moFolder.IsDirectory) return ErrorResponse.Success;
return ErrorResponse.Fail($"MO2 folder could not be located from the given ModList location.{Environment.NewLine}Make sure your ModList is inside a valid MO2 distribution.");
});
// Load custom ModList settings per MO2 profile
_modlistSettings = Observable.CombineLatest(
(this).WhenAny(x => x.ModListLocation.ErrorState),
(this).WhenAny(x => x.ModListLocation.TargetPath),
resultSelector: (state, path) => (State: state, Path: path))
// A short throttle is a quick hack to make the above changes "atomic"
.Throttle(TimeSpan.FromMilliseconds(25), RxApp.MainThreadScheduler)
.Select(u =>
{
if (u.State.Failed) return null;
var modlistSettings = _settings.ModlistSettings.TryCreate(u.Path);
return new ModlistSettingsEditorVM(modlistSettings)
{
ModListName = MOProfile
};
})
// Interject and save old while loading new
.Pairwise()
.Do(pair =>
{
pair.Previous?.Save();
pair.Current?.Init();
})
.Select(x => x.Current)
.ToGuiProperty(this, nameof(ModlistSettings));
CanCompile = Observable.CombineLatest(
this.WhenAny(x => x.ModListLocation.InError),
this.WhenAny(x => x.DownloadLocation.InError),
parent.WhenAny(x => x.OutputLocation.InError),
this.WhenAny(x => x.ModlistSettings)
.Select(x => x?.InError ?? Observable.Return(false))
.Switch(),
resultSelector: (ml, down, output, modlistSettings) => !ml && !down && !output && !modlistSettings)
.Publish()
.RefCount();
// Load settings
_settings = parent.MWVM.Settings.Compiler.MO2Compilation;
ModListLocation.TargetPath = _settings.LastCompiledProfileLocation;
if (_settings.DownloadLocation != default)
{
DownloadLocation.TargetPath = _settings.DownloadLocation;
}
parent.MWVM.Settings.SaveSignal
.Subscribe(_ => Unload())
.DisposeWith(CompositeDisposable);
// If Mo2 folder changes and download location is empty, set it for convenience
this.WhenAny(x => x.Mo2Folder)
.DelayInitial(TimeSpan.FromMilliseconds(100), RxApp.MainThreadScheduler)
.Where(x => x.IsDirectory)
.FlowSwitch(
(this).WhenAny(x => x.DownloadLocation.Exists)
.Invert())
// A skip is needed to ignore the initial signal when the FilterSwitch turns on
.Skip(1)
.Subscribe(_ =>
{
DownloadLocation.TargetPath = MO2Compiler.GetTypicalDownloadsFolder(Mo2Folder);
})
.DisposeWith(CompositeDisposable);
}
public void Unload()
{
_settings.DownloadLocation = DownloadLocation.TargetPath;
_settings.LastCompiledProfileLocation = ModListLocation.TargetPath;
ModlistSettings?.Save();
}
public async Task<GetResponse<ModList>> Compile()
{
AbsolutePath outputFile;
var profileName = string.IsNullOrWhiteSpace(ModlistSettings.ModListName)
? MOProfile
: ModlistSettings.ModListName;
if (Parent.OutputLocation.TargetPath == default)
{
outputFile = (profileName + Consts.ModListExtension).RelativeTo(AbsolutePath.EntryPoint);
}
else
{
outputFile = Parent.OutputLocation.TargetPath.Combine(profileName + Consts.ModListExtension);
}
try
{
ACompiler compiler;
UpdateRequest request = null;
if (ModlistSettings.Publish)
{
request = new UpdateRequest
{
MachineUrl = ModlistSettings.MachineUrl.Trim(),
Version = ModlistSettings.Version,
};
}
if (ModListLocation.TargetPath.FileName == Consts.NativeSettingsJson)
{
var settings = ModListLocation.TargetPath.FromJson<NativeCompilerSettings>();
compiler = new NativeCompiler(settings, Mo2Folder, DownloadLocation.TargetPath, outputFile, request)
{
ModListName = ModlistSettings.ModListName,
ModListAuthor = ModlistSettings.AuthorText,
ModListDescription = ModlistSettings.Description,
ModListImage = ModlistSettings.ImagePath.TargetPath,
ModListWebsite = ModlistSettings.Website,
ModlistReadme = ModlistSettings.Readme,
ModlistVersion = ModlistSettings.Version,
ModlistIsNSFW = ModlistSettings.IsNSFW
};
}
else
{
compiler = new MO2Compiler(
sourcePath: Mo2Folder,
downloadsPath: DownloadLocation.TargetPath,
mo2Profile: MOProfile,
outputFile: outputFile,
publishData: request)
{
ModListName = ModlistSettings.ModListName,
ModListAuthor = ModlistSettings.AuthorText,
ModListDescription = ModlistSettings.Description,
ModListImage = ModlistSettings.ImagePath.TargetPath,
ModListWebsite = ModlistSettings.Website,
ModlistReadme = ModlistSettings.Readme,
ModlistVersion = ModlistSettings.Version,
ModlistIsNSFW = ModlistSettings.IsNSFW
};
}
using (ActiveCompilation = compiler)
{
Parent.MWVM.Settings.Performance.SetProcessorSettings(ActiveCompilation);
var success = await ActiveCompilation.Begin();
return GetResponse<ModList>.Create(success, ActiveCompilation.ModList);
}
}
finally
{
StatusTracker = null;
ActiveCompilation = null;
}
}
}
}

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.Lib;
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,209 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using DynamicData;
using DynamicData.Binding;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.Common;
using Wabbajack.Lib;
using Wabbajack.Lib.ModListRegistry;
namespace Wabbajack
{
public class ModListGalleryVM : BackNavigatingVM
{
public MainWindowVM MWVM { get; }
public ObservableCollectionExtended<ModListMetadataVM> ModLists { get; } = new ObservableCollectionExtended<ModListMetadataVM>();
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 { get { return GetGameTypeEntries(); } }
private readonly ObservableAsPropertyHelper<bool> _Loaded;
private FiltersSettings settings => MWVM.Settings.Filters;
public bool Loaded => _Loaded.Value;
public ICommand ClearFiltersCommand { get; }
public ModListGalleryVM(MainWindowVM mainWindowVM)
: base(mainWindowVM)
{
MWVM = mainWindowVM;
// load persistent filter settings
if (settings.IsPersistent)
{
GameType = !string.IsNullOrEmpty(settings.Game) ? settings.Game : ALL_GAME_TYPE;
ShowNSFW = settings.ShowNSFW;
ShowUtilityLists = settings.ShowUtilityLists;
OnlyInstalled = settings.OnlyInstalled;
Search = settings.Search;
}
else
GameType = ALL_GAME_TYPE;
// subscribe to save signal
MWVM.Settings.SaveSignal
.Subscribe(_ => UpdateFiltersSettings())
.DisposeWith(this.CompositeDisposable);
ClearFiltersCommand = ReactiveCommand.Create(
() =>
{
OnlyInstalled = false;
ShowNSFW = false;
ShowUtilityLists = false;
Search = string.Empty;
GameType = ALL_GAME_TYPE;
});
this.WhenAny(x => x.OnlyInstalled)
.Subscribe(val =>
{
if(val)
GameType = ALL_GAME_TYPE;
})
.DisposeWith(CompositeDisposable);
var sourceList = Observable.Return(Unit.Default)
.ObserveOn(RxApp.TaskpoolScheduler)
.SelectTask(async _ =>
{
try
{
Error = null;
var list = await ModlistMetadata.LoadFromGithub();
Error = ErrorResponse.Success;
return list
.AsObservableChangeSet(x => x.DownloadMetadata?.Hash ?? Hash.Empty);
}
catch (Exception ex)
{
Utils.Error(ex);
Error = ErrorResponse.Fail(ex);
return Observable.Empty<IChangeSet<ModlistMetadata, Hash>>();
}
})
// Unsubscribe and release when not active
.FlowSwitch(
this.WhenAny(x => x.IsActive),
valueWhenOff: Observable.Return(ChangeSet<ModlistMetadata, Hash>.Empty))
.Switch()
.RefCount();
_Loaded = sourceList.CollectionCount()
.Select(c => c > 0)
.ToProperty(this, nameof(Loaded));
// Convert to VM and bind to resulting list
sourceList
.ObserveOnGuiThread()
.Transform(m => new ModListMetadataVM(this, m))
.DisposeMany()
// Filter only installed
.Filter(this.WhenAny(x => x.OnlyInstalled)
.Select<bool, Func<ModListMetadataVM, bool>>(onlyInstalled => (vm) =>
{
if (!onlyInstalled) return true;
if (!GameRegistry.Games.TryGetValue(vm.Metadata.Game, out var gameMeta)) return false;
return gameMeta.IsInstalled;
}))
// Filter on search box
.Filter(this.WhenAny(x => x.Search)
.Debounce(TimeSpan.FromMilliseconds(150), RxApp.MainThreadScheduler)
.Select<string, Func<ModListMetadataVM, bool>>(search => (vm) =>
{
if (string.IsNullOrWhiteSpace(search)) return true;
return vm.Metadata.Title.ContainsCaseInsensitive(search) || vm.Metadata.tags.Any(t => t.ContainsCaseInsensitive(search));
}))
.Filter(this.WhenAny(x => x.ShowNSFW)
.Select<bool, Func<ModListMetadataVM, bool>>(showNSFW => vm =>
{
if (!vm.Metadata.NSFW) return true;
return vm.Metadata.NSFW && showNSFW;
}))
.Filter(this.WhenAny(x => x.ShowUtilityLists)
.Select<bool, Func<ModListMetadataVM, bool>>(showUtilityLists => vm => showUtilityLists ? vm.Metadata.UtilityList : !vm.Metadata.UtilityList))
// Filter by Game
.Filter(this.WhenAny(x => x.GameType)
.Debounce(TimeSpan.FromMilliseconds(150), RxApp.MainThreadScheduler)
.Select<string, Func<ModListMetadataVM, bool>>(GameType => (vm) =>
{
if (GameType == ALL_GAME_TYPE)
return true;
if (string.IsNullOrEmpty(GameType))
return false;
return GameType == vm.Metadata.Game.GetDescription<Game>().ToString();
}))
.Bind(ModLists)
.Subscribe()
.DisposeWith(CompositeDisposable);
// Extra GC when navigating away, just to immediately clean up modlist metadata
this.WhenAny(x => x.IsActive)
.Where(x => !x)
.Skip(1)
.Delay(TimeSpan.FromMilliseconds(50), RxApp.MainThreadScheduler)
.Subscribe(_ =>
{
GC.Collect();
})
.DisposeWith(CompositeDisposable);
}
public override void Unload()
{
Error = null;
}
private List<string> GetGameTypeEntries()
{
List<string> gameEntries = new List<string> { ALL_GAME_TYPE };
gameEntries.AddRange(EnumExtensions.GetAllItems<Game>().Select(gameType => gameType.GetDescription<Game>()));
gameEntries.Sort();
return gameEntries;
}
private void UpdateFiltersSettings()
{
settings.Game = GameType;
settings.Search = Search;
settings.ShowNSFW = ShowNSFW;
settings.ShowUtilityLists = ShowUtilityLists;
settings.OnlyInstalled = OnlyInstalled;
}
}
}

View File

@ -0,0 +1,261 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media.Imaging;
using Alphaleonis.Win32.Filesystem;
using DynamicData;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.Common;
using Wabbajack.Lib;
using Wabbajack.Lib.Downloaders;
using Wabbajack.Lib.ModListRegistry;
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; }
[Reactive]
public List<ModListTag> ModListTagList { get; private set; }
[Reactive]
public Percent ProgressPercent { get; private set; }
[Reactive]
public bool IsBroken { get; private 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;
public ModListMetadataVM(ModListGalleryVM parent, ModlistMetadata metadata)
{
_parent = parent;
Metadata = metadata;
Location = LauncherUpdater.CommonFolder.Value.Combine("downloaded_mod_lists", Metadata.Links.MachineURL + (string)Consts.ModListExtension);
ModListTagList = new List<ModListTag>();
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(() => Utils.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 ClientAPIEx.GetDetailedStatus(metadata.Links.MachineURL);
var coll = _parent.MWVM.ModListContentsVM.Value.Status;
coll.Clear();
coll.AddRange(status.Archives);
_parent.MWVM.NavigateTo(_parent.MWVM.ModListContentsVM.Value);
}
finally
{
IsLoadingIdle.OnNext(true);
}
}, IsLoadingIdle.StartWith(true));
ExecuteCommand = ReactiveCommand.CreateFromObservable<Unit, Unit>(
canExecute: this.WhenAny(x => x.IsBroken).Select(x => !x),
execute: (unit) =>
Observable.Return(unit)
.WithLatestFrom(
this.WhenAny(x => x.Exists),
(_, e) => e)
// Do any download work on background thread
.ObserveOn(RxApp.TaskpoolScheduler)
.SelectTask(async (exists) =>
{
if (!exists)
{
try
{
var success = await Download();
if (!success)
{
Error = ErrorResponse.Fail("Download was marked unsuccessful");
return false;
}
}
catch (Exception ex)
{
Error = ErrorResponse.Fail(ex);
return false;
}
// Return an updated check on exists
return Location.Exists;
}
return exists;
})
.Where(exists => exists)
// Do any install page swap over on GUI thread
.ObserveOnGuiThread()
.Select(_ =>
{
_parent.MWVM.OpenInstaller(Location);
// Wait for modlist member to be filled, then open its readme
return _parent.MWVM.Installer.Value.WhenAny(x => x.ModList)
.NotNull()
.Take(1)
.Do(modList =>
{
try
{
modList.OpenReadme();
}
catch (Exception ex)
{
Utils.Error(ex);
}
});
})
.Switch()
.Unit());
_Exists = Observable.Interval(TimeSpan.FromSeconds(0.5))
.Unit()
.StartWith(Unit.Default)
.FlowSwitch(_parent.WhenAny(x => x.IsActive))
.SelectAsync(async _ =>
{
try
{
return !IsDownloading && !(await metadata.NeedsDownload(Location));
}
catch (Exception)
{
return true;
}
})
.ToGuiProperty(this, nameof(Exists));
var imageObs = Observable.Return(Metadata.Links.ImageUri)
.DownloadBitmapImage((ex) => Utils.Log($"Error downloading modlist image {Metadata.Title}"));
_Image = imageObs
.ToGuiProperty(this, nameof(Image));
_LoadingImage = imageObs
.Select(x => false)
.StartWith(true)
.ToGuiProperty(this, nameof(LoadingImage));
}
private async Task<bool> Download()
{
ProgressPercent = Percent.Zero;
using (var queue = new WorkQueue(1))
using (queue.Status.Select(i => i.ProgressPercent)
.ObserveOnGuiThread()
.Subscribe(percent => ProgressPercent = percent))
{
var tcs = new TaskCompletionSource<bool>();
queue.QueueTask(async () =>
{
try
{
IsDownloading = true;
Utils.Log($"Starting Download of {Metadata.Links.MachineURL}");
var downloader = DownloadDispatcher.ResolveArchive(Metadata.Links.Download);
var result = await downloader.Download(
new Archive(state: null!)
{
Name = Metadata.Title, Size = Metadata.DownloadMetadata?.Size ?? 0
}, Location);
Utils.Log($"Done downloading {Metadata.Links.MachineURL}");
// Want to rehash to current file, even if failed?
await Location.FileHashCachedAsync();
Utils.Log($"Done hashing {Metadata.Links.MachineURL}");
await Metadata.ToJsonAsync(Location.WithExtension(Consts.ModlistMetadataExtension));
tcs.SetResult(result);
}
catch (Exception ex)
{
Utils.Error(ex, $"Error Downloading of {Metadata.Links.MachineURL}");
tcs.SetException(ex);
}
finally
{
IsDownloading = false;
}
});
Task.Run(async () => await Metrics.Send(Metrics.Downloading, Metadata.Title))
.FireAndForget(ex => Utils.Error(ex, "Error sending download metric"));
return await tcs.Task;
}
}
}
}

View File

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

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ReactiveUI;
using Wabbajack.Common;
using Wabbajack.Lib;
namespace Wabbajack
{
public interface ISubInstallerVM
{
InstallerVM Parent { get; }
AInstaller ActiveInstallation { get; }
void Unload();
bool SupportsAfterInstallNavigation { get; }
void AfterInstallNavigation();
int ConfigVisualVerticalOffset { get; }
ErrorResponse CanInstall { get; }
Task<bool> Install();
IUserIntervention InterventionConverter(IUserIntervention intervention);
}
}

View File

@ -0,0 +1,461 @@
using System;
using ReactiveUI;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media.Imaging;
using Wabbajack.Common;
using Wabbajack.Lib;
using ReactiveUI.Fody.Helpers;
using System.Windows.Media;
using DynamicData;
using DynamicData.Binding;
using Wabbajack.Common.StatusFeed;
using System.Reactive;
using System.Collections.Generic;
using System.Reactive.Subjects;
using System.Windows.Input;
using Microsoft.WindowsAPICodePack.Dialogs;
using Wabbajack.Common.IO;
namespace Wabbajack
{
public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM
{
public SlideShow Slideshow { get; }
public MainWindowVM MWVM { get; }
private readonly ObservableAsPropertyHelper<ModListVM> _modList;
public ModListVM ModList => _modList.Value;
public FilePickerVM ModListLocation { get; }
private readonly ObservableAsPropertyHelper<ISubInstallerVM> _installer;
public ISubInstallerVM Installer => _installer.Value;
private readonly ObservableAsPropertyHelper<bool> _installing;
public bool Installing => _installing.Value;
[Reactive]
public bool StartedInstallation { get; set; }
[Reactive]
public ErrorResponse? Completed { get; set; }
private readonly ObservableAsPropertyHelper<ImageSource> _image;
public ImageSource Image => _image.Value;
private readonly ObservableAsPropertyHelper<string> _titleText;
public string TitleText => _titleText.Value;
private readonly ObservableAsPropertyHelper<string> _authorText;
public string AuthorText => _authorText.Value;
private readonly ObservableAsPropertyHelper<string> _description;
public string Description => _description.Value;
private readonly ObservableAsPropertyHelper<string> _progressTitle;
public string ProgressTitle => _progressTitle.Value;
private readonly ObservableAsPropertyHelper<string> _modListName;
public string ModListName => _modListName.Value;
private readonly ObservableAsPropertyHelper<Percent> _percentCompleted;
public Percent PercentCompleted => _percentCompleted.Value;
public ObservableCollectionExtended<CPUDisplayVM> StatusList { get; } = new ObservableCollectionExtended<CPUDisplayVM>();
public ObservableCollectionExtended<IStatusMessage> Log => MWVM.Log;
private readonly ObservableAsPropertyHelper<ModManager?> _TargetManager;
public ModManager? TargetManager => _TargetManager.Value;
private readonly ObservableAsPropertyHelper<IUserIntervention> _ActiveGlobalUserIntervention;
public IUserIntervention ActiveGlobalUserIntervention => _ActiveGlobalUserIntervention.Value;
private readonly ObservableAsPropertyHelper<(int CurrentCPUs, int DesiredCPUs)> _CurrentCpuCount;
public (int CurrentCPUs, int DesiredCPUs) CurrentCpuCount => _CurrentCpuCount.Value;
private readonly ObservableAsPropertyHelper<bool> _LoadingModlist;
public bool LoadingModlist => _LoadingModlist.Value;
// Command properties
public ReactiveCommand<Unit, Unit> ShowManifestCommand { get; }
public ReactiveCommand<Unit, Unit> OpenReadmeCommand { get; }
public ReactiveCommand<Unit, Unit> VisitModListWebsiteCommand { get; }
public ReactiveCommand<Unit, Unit> CloseWhenCompleteCommand { get; }
public ReactiveCommand<Unit, Unit> OpenLogsCommand { get; }
public ReactiveCommand<Unit, Unit> GoToInstallCommand { get; }
public ReactiveCommand<Unit, Unit> BeginCommand { get; }
public InstallerVM(MainWindowVM mainWindowVM) : base(mainWindowVM)
{
var downloadsPath = KnownFolders.Downloads.Path;
var skyDrivePath = KnownFolders.SkyDrive.Path;
if (downloadsPath != null && AbsolutePath.EntryPoint.IsChildOf(new AbsolutePath(downloadsPath)))
{
Utils.Error(new CriticalFailureIntervention(
"Wabbajack is running inside your Downloads folder. This folder is often highly monitored by antivirus software and these can often " +
"conflict with the operations Wabbajack needs to perform. Please move Wabbajack outside of your Downloads folder and then restart the app.",
"Cannot run inside Downloads", true));
}
if (skyDrivePath != null && AbsolutePath.EntryPoint.IsChildOf(new AbsolutePath(skyDrivePath)))
{
Utils.Error(new CriticalFailureIntervention(
$"Wabbajack is running inside a OneDrive folder \"{skyDrivePath}\". This folder is known to cause issues with Wabbajack. " +
"Please move Wabbajack outside of your OneDrive folder and then restart the app.",
"Cannot run inside OneDrive", true));
}
MWVM = mainWindowVM;
ModListLocation = new FilePickerVM
{
ExistCheckOption = FilePickerVM.CheckOptions.On,
PathType = FilePickerVM.PathTypeOptions.File,
PromptTitle = "Select a ModList to install"
};
ModListLocation.Filters.Add(new CommonFileDialogFilter("Wabbajack Modlist", "*.wabbajack"));
// Swap to proper sub VM based on selected type
_installer = this.WhenAny(x => x.TargetManager)
// Delay so the initial VM swap comes in immediately, subVM comes right after
.DelayInitial(TimeSpan.FromMilliseconds(50), RxApp.MainThreadScheduler)
.Select<ModManager?, ISubInstallerVM>(type =>
{
switch (type)
{
case ModManager.MO2:
return new MO2InstallerVM(this);
default:
return null;
}
})
// Unload old VM
.Pairwise()
.Do(pair =>
{
pair.Previous?.Unload();
})
.Select(p => p.Current)
.ToGuiProperty(this, nameof(Installer));
// Load settings
MWVM.Settings.SaveSignal
.Subscribe(_ =>
{
MWVM.Settings.Installer.LastInstalledListLocation = ModListLocation.TargetPath;
})
.DisposeWith(CompositeDisposable);
_IsActive = this.ConstructIsActive(MWVM)
.ToGuiProperty(this, nameof(IsActive));
// Active path represents the path to currently have loaded
// If we're not actively showing, then "unload" the active path
var activePath = Observable.CombineLatest(
this.WhenAny(x => x.ModListLocation.TargetPath),
this.WhenAny(x => x.IsActive),
resultSelector: (path, active) => (path, active))
.Select(x =>
{
if (!x.active) return default;
return x.path;
})
// Throttle slightly so changes happen more atomically
.Throttle(TimeSpan.FromMilliseconds(50), RxApp.MainThreadScheduler)
.Replay(1)
.RefCount();
_modList = activePath
.ObserveOn(RxApp.TaskpoolScheduler)
// Convert from active path to modlist VM
.Select(modListPath =>
{
if (modListPath == default) return default;
if (!modListPath.Exists) return default;
return new ModListVM(modListPath);
})
.DisposeOld()
.ObserveOnGuiThread()
.StartWith(default(ModListVM))
.ToGuiProperty(this, nameof(ModList));
// Force GC collect when modlist changes, just to make sure we clean up any loose large items immediately
this.WhenAny(x => x.ModList)
.Delay(TimeSpan.FromMilliseconds(50), RxApp.MainThreadScheduler)
.Subscribe(x =>
{
GC.Collect();
});
_LoadingModlist = Observable.Merge(
// When active path changes, mark as loading
activePath
.Select(_ => true),
// When the resulting modlist comes in, mark it as done
this.WhenAny(x => x.ModList)
.Select(_ => false))
.ToGuiProperty(this, nameof(LoadingModlist));
_installing = this.WhenAny(x => x.Installer.ActiveInstallation)
.Select(i => i != null)
.ToGuiProperty(this, nameof(Installing));
_TargetManager = this.WhenAny(x => x.ModList)
.Select(modList => modList?.ModManager)
.ToGuiProperty(this, nameof(TargetManager));
// Add additional error check on ModList
ModListLocation.AdditionalError = this.WhenAny(x => x.ModList)
.Select<ModListVM, IErrorResponse>(modList =>
{
if (modList == null) return ErrorResponse.Fail("Modlist path resulted in a null object.");
if (modList.Error != null) return ErrorResponse.Fail("Modlist is corrupt", modList.Error);
if (modList.WabbajackVersion != null && modList.WabbajackVersion > Consts.CurrentMinimumWabbajackVersion)
return ErrorResponse.Fail("The Modlist you are trying to install was made using a newer Version of Wabbajack. Please update Wabbajack before installing!");
return ErrorResponse.Success;
});
BackCommand = ReactiveCommand.Create(
execute: () =>
{
StartedInstallation = false;
Completed = null;
mainWindowVM.NavigateTo(mainWindowVM.ModeSelectionVM);
},
canExecute: Observable.CombineLatest(
this.WhenAny(x => x.Installing)
.Select(x => !x),
this.ConstructCanNavigateBack(),
resultSelector: (i, b) => i && b)
.ObserveOnGuiThread());
_percentCompleted = this.WhenAny(x => x.Installer.ActiveInstallation)
.StartWith(default(AInstaller))
.CombineLatest(
this.WhenAny(x => x.Completed),
(installer, completed) =>
{
if (installer == null)
{
return Observable.Return<Percent>(completed != null ? Percent.One : Percent.Zero);
}
return installer.PercentCompleted.StartWith(Percent.Zero);
})
.Switch()
.Debounce(TimeSpan.FromMilliseconds(25), RxApp.MainThreadScheduler)
.ToGuiProperty(this, nameof(PercentCompleted));
Slideshow = new SlideShow(this);
// Set display items to ModList if configuring or complete,
// or to the current slideshow data if installing
_image = Observable.CombineLatest(
this.WhenAny(x => x.ModList.Error),
this.WhenAny(x => x.ModList)
.Select(x => x?.ImageObservable ?? Observable.Return(default(BitmapImage)))
.Switch()
.StartWith(default(BitmapImage)),
this.WhenAny(x => x.Slideshow.Image)
.StartWith(default(BitmapImage)),
this.WhenAny(x => x.Installing),
this.WhenAny(x => x.LoadingModlist),
resultSelector: (err, modList, slideshow, installing, loading) =>
{
if (err != null)
{
return ResourceLinks.WabbajackErrLogo.Value;
}
if (loading) return default;
return installing ? slideshow : modList;
})
.Select<BitmapImage, ImageSource>(x => x)
.ToGuiProperty(this, nameof(Image));
_titleText = Observable.CombineLatest(
this.WhenAny(x => x.ModList)
.Select(modList => modList?.Name ?? string.Empty),
this.WhenAny(x => x.Slideshow.TargetMod.State.Name)
.StartWith(default(string)),
this.WhenAny(x => x.Installing),
resultSelector: (modList, mod, installing) => installing ? mod : modList)
.ToGuiProperty(this, nameof(TitleText));
_authorText = Observable.CombineLatest(
this.WhenAny(x => x.ModList)
.Select(modList => modList?.Author ?? string.Empty),
this.WhenAny(x => x.Slideshow.TargetMod.State.Author)
.StartWith(default(string)),
this.WhenAny(x => x.Installing),
resultSelector: (modList, mod, installing) => installing ? mod : modList)
.ToGuiProperty(this, nameof(AuthorText));
_description = Observable.CombineLatest(
this.WhenAny(x => x.ModList)
.Select(modList => modList?.Description ?? string.Empty),
this.WhenAny(x => x.Slideshow.TargetMod.State.Description)
.StartWith(default(string)),
this.WhenAny(x => x.Installing),
resultSelector: (modList, mod, installing) => installing ? mod : modList)
.ToGuiProperty(this, nameof(Description));
_modListName = Observable.CombineLatest(
this.WhenAny(x => x.ModList.Error)
.Select(x => x != null),
this.WhenAny(x => x.ModList)
.Select(x => x?.Name),
resultSelector: (err, name) =>
{
if (err) return "Corrupted Modlist";
return name;
})
.Merge(this.WhenAny(x => x.Installer.ActiveInstallation)
.Where(c => c != null)
.SelectMany(c => c.TextStatus))
.ToGuiProperty(this, nameof(ModListName));
ShowManifestCommand = ReactiveCommand.Create(() =>
{
Utils.OpenWebsite(new Uri("https://www.wabbajack.org/#/modlists/manifest"));
}, this.WhenAny(x => x.ModList)
.Select(x => x?.SourceModList != null)
.ObserveOnGuiThread());
OpenReadmeCommand = ReactiveCommand.Create(
execute: () => this.ModList?.OpenReadme(),
canExecute: this.WhenAny(x => x.ModList)
.Select(modList => !string.IsNullOrEmpty(modList?.Readme))
.ObserveOnGuiThread());
OpenLogsCommand = ReactiveCommand.Create(
execute: () => Utils.OpenFolder(Consts.LogsFolder));
VisitModListWebsiteCommand = ReactiveCommand.Create(
execute: () =>
{
Utils.OpenWebsite(ModList.Website);
return Unit.Default;
},
canExecute: this.WhenAny(x => x.ModList.Website)
.Select(x => x != null)
.ObserveOnGuiThread());
_progressTitle = this.WhenAnyValue(
x => x.Installing,
x => x.StartedInstallation,
x => x.Completed,
selector: (installing, started, completed) =>
{
if (installing)
{
return "Installing";
}
else if (started)
{
if (completed == null) return "Installing";
return completed.Value.Succeeded ? "Installed" : "Failed";
}
else
{
return "Awaiting Input";
}
})
.ToGuiProperty(this, nameof(ProgressTitle));
UIUtils.BindCpuStatus(
this.WhenAny(x => x.Installer.ActiveInstallation)
.SelectMany(c => c?.QueueStatus ?? Observable.Empty<CPUStatus>()),
StatusList)
.DisposeWith(CompositeDisposable);
BeginCommand = ReactiveCommand.CreateFromTask(
canExecute: this.WhenAny(x => x.Installer.CanInstall)
.Select(err => err.Succeeded),
execute: async () =>
{
try
{
Utils.Log($"Starting to install {ModList.Name}");
IsBackEnabledSubject.OnNext(false);
var success = await this.Installer.Install();
Completed = ErrorResponse.Create(success);
try
{
this.ModList?.OpenReadme();
}
catch (Exception ex)
{
Utils.Error(ex);
}
}
catch (Exception ex)
{
Utils.Error(ex, $"Encountered error, can't continue");
while (ex.InnerException != null) ex = ex.InnerException;
Completed = ErrorResponse.Fail(ex);
}
finally
{
IsBackEnabledSubject.OnNext(true);
}
});
// When sub installer begins an install, mark state variable
BeginCommand.StartingExecution()
.Subscribe(_ =>
{
StartedInstallation = true;
})
.DisposeWith(CompositeDisposable);
// Listen for user interventions, and compile a dynamic list of all unhandled ones
var activeInterventions = this.WhenAny(x => x.Installer.ActiveInstallation)
.WithLatestFrom(
this.WhenAny(x => x.Installer),
(activeInstall, installer) =>
{
if (activeInstall == null) return Observable.Empty<IChangeSet<IUserIntervention>>();
return activeInstall.LogMessages
.WhereCastable<IStatusMessage, IUserIntervention>()
.ToObservableChangeSet()
.AutoRefresh(i => i.Handled)
.Filter(i => !i.Handled)
.Transform(x => installer.InterventionConverter(x));
})
.Switch()
.AsObservableList();
// Find the top intervention /w no CPU ID to be marked as "global"
_ActiveGlobalUserIntervention = activeInterventions.Connect()
.Filter(x => x.CpuID == WorkQueue.UnassignedCpuId)
.QueryWhenChanged(query => query.FirstOrDefault())
.ToGuiProperty(this, nameof(ActiveGlobalUserIntervention));
CloseWhenCompleteCommand = ReactiveCommand.CreateFromTask(
canExecute: this.WhenAny(x => x.Completed)
.Select(x => x != null),
execute: async () =>
{
await MWVM.ShutdownApplication();
});
GoToInstallCommand = ReactiveCommand.Create(
canExecute: Observable.CombineLatest(
this.WhenAny(x => x.Completed)
.Select(x => x != null),
this.WhenAny(x => x.Installer.SupportsAfterInstallNavigation),
resultSelector: (complete, supports) => complete && supports),
execute: () =>
{
Installer.AfterInstallNavigation();
});
_CurrentCpuCount = this.WhenAny(x => x.Installer.ActiveInstallation.Queue.CurrentCpuCount)
.Switch()
.ToGuiProperty(this, nameof(CurrentCpuCount));
}
}
}

View File

@ -0,0 +1,202 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.Common;
using Wabbajack.Common.StatusFeed;
using Wabbajack.Lib;
using Wabbajack.Util;
namespace Wabbajack
{
public class MO2InstallerVM : ViewModel, ISubInstallerVM
{
public InstallerVM Parent { get; }
private readonly ObservableAsPropertyHelper<ErrorResponse> _CanInstall;
public ErrorResponse CanInstall => _CanInstall.Value;
[Reactive]
public AInstaller ActiveInstallation { get; private set; }
private readonly ObservableAsPropertyHelper<Mo2ModlistInstallationSettings> _CurrentSettings;
public Mo2ModlistInstallationSettings CurrentSettings => _CurrentSettings.Value;
public FilePickerVM Location { get; }
public FilePickerVM DownloadLocation { get; }
public bool SupportsAfterInstallNavigation => true;
[Reactive]
public bool AutomaticallyOverwrite { get; set; }
public int ConfigVisualVerticalOffset => 25;
public MO2InstallerVM(InstallerVM installerVM)
{
Parent = installerVM;
Location = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.CheckOptions.Off,
PathType = FilePickerVM.PathTypeOptions.Folder,
PromptTitle = "Select Installation Directory",
};
DownloadLocation = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.CheckOptions.Off,
PathType = FilePickerVM.PathTypeOptions.Folder,
PromptTitle = "Select a location for MO2 downloads",
};
DownloadLocation.AdditionalError = this.WhenAny(x => x.DownloadLocation.TargetPath)
.Select(x => Utils.IsDirectoryPathValid(x));
Location.AdditionalError = Observable.CombineLatest(
this.WhenAny(x => x.Location.TargetPath),
this.WhenAny(x => x.DownloadLocation.TargetPath),
resultSelector: (target, download) => (target, download))
.ObserveOn(RxApp.TaskpoolScheduler)
.Select(i => MO2Installer.CheckValidInstallPath(i.target, i.download, Parent.ModList?.SourceModList?.GameType.MetaData()))
.ObserveOnGuiThread();
_CanInstall = Observable.CombineLatest(
this.WhenAny(x => x.Location.ErrorState),
this.WhenAny(x => x.DownloadLocation.ErrorState),
installerVM.WhenAny(x => x.ModListLocation.ErrorState),
resultSelector: (loc, modlist, download) =>
{
return ErrorResponse.FirstFail(loc, modlist, download);
})
.ToProperty(this, nameof(CanInstall));
// Have Installation location updates modify the downloads location if empty or the same path
this.WhenAny(x => x.Location.TargetPath)
.Skip(1) // Don't do it initially
.Subscribe(installPath =>
{
if (DownloadLocation.TargetPath == default || DownloadLocation.TargetPath == installPath)
{
if (installPath.Exists) DownloadLocation.TargetPath = installPath.Combine("downloads");
}
})
.DisposeWith(CompositeDisposable);
// Have Download location updates change if the same as the install path
this.WhenAny(x => x.DownloadLocation.TargetPath)
.Skip(1) // Don't do it initially
.Subscribe(downloadPath =>
{
if (downloadPath != default && downloadPath == Location.TargetPath)
{
DownloadLocation.TargetPath = Location.TargetPath.Combine("downloads");
}
})
.DisposeWith(CompositeDisposable);
// Load settings
_CurrentSettings = installerVM.WhenAny(x => x.ModListLocation.TargetPath)
.Select(path => path == default ? null : installerVM.MWVM.Settings.Installer.Mo2ModlistSettings.TryCreate(path))
.ToGuiProperty(this, nameof(CurrentSettings));
this.WhenAny(x => x.CurrentSettings)
.Pairwise()
.Subscribe(settingsPair =>
{
SaveSettings(settingsPair.Previous);
if (settingsPair.Current == null) return;
Location.TargetPath = settingsPair.Current.InstallationLocation;
DownloadLocation.TargetPath = settingsPair.Current.DownloadLocation;
AutomaticallyOverwrite = settingsPair.Current.AutomaticallyOverrideExistingInstall;
})
.DisposeWith(CompositeDisposable);
installerVM.MWVM.Settings.SaveSignal
.Subscribe(_ => SaveSettings(CurrentSettings))
.DisposeWith(CompositeDisposable);
// Hook onto user interventions, and intercept MO2 specific ones for customization
this.WhenAny(x => x.ActiveInstallation)
.Select(x => x?.LogMessages ?? Observable.Empty<IStatusMessage>())
.Switch()
.Subscribe(x =>
{
switch (x)
{
case ConfirmUpdateOfExistingInstall c:
if (AutomaticallyOverwrite)
{
c.Confirm();
}
break;
default:
break;
}
})
.DisposeWith(CompositeDisposable);
}
public void Unload()
{
SaveSettings(this.CurrentSettings);
}
private void SaveSettings(Mo2ModlistInstallationSettings settings)
{
Parent.MWVM.Settings.Installer.LastInstalledListLocation = Parent.ModListLocation.TargetPath;
if (settings == null) return;
settings.InstallationLocation = Location.TargetPath;
settings.DownloadLocation = DownloadLocation.TargetPath;
settings.AutomaticallyOverrideExistingInstall = AutomaticallyOverwrite;
}
public void AfterInstallNavigation()
{
Process.Start("explorer.exe", (string)Location.TargetPath);
}
public async Task<bool> Install()
{
using (var installer = new MO2Installer(
archive: Parent.ModListLocation.TargetPath,
modList: Parent.ModList.SourceModList,
outputFolder: Location.TargetPath,
downloadFolder: DownloadLocation.TargetPath,
parameters: SystemParametersConstructor.Create()))
{
installer.Metadata = Parent.ModList.SourceModListMetadata;
installer.UseCompression = Parent.MWVM.Settings.Filters.UseCompression;
Parent.MWVM.Settings.Performance.SetProcessorSettings(installer);
return await Task.Run(async () =>
{
try
{
var workTask = installer.Begin();
ActiveInstallation = installer;
return await workTask;
}
finally
{
ActiveInstallation = null;
}
});
}
}
public IUserIntervention InterventionConverter(IUserIntervention intervention)
{
switch (intervention)
{
case ConfirmUpdateOfExistingInstall confirm:
return new ConfirmUpdateOfExistingInstallVM(this, confirm);
default:
return intervention;
}
}
}
}

View File

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DynamicData.Binding;
using ReactiveUI;
namespace Wabbajack
{
public interface ICpuStatusVM : IReactiveObject
{
ObservableCollectionExtended<CPUDisplayVM> StatusList { get; }
MainWindowVM MWVM { get; }
(int CurrentCPUs, int DesiredCPUs) CurrentCpuCount { get; }
}
}

View File

@ -0,0 +1,191 @@
using DynamicData;
using DynamicData.Binding;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using Alphaleonis.Win32.Filesystem;
using Wabbajack.Common;
using Wabbajack.Common.StatusFeed;
using Wabbajack.Lib;
namespace Wabbajack
{
/// <summary>
/// Main View Model for the application.
/// Keeps track of which sub view is being shown in the window, and has some singleton wiring like WorkQueue and Logging.
/// </summary>
public class MainWindowVM : ViewModel
{
public MainWindow MainWindow { get; }
public MainSettings Settings { get; }
[Reactive]
public ViewModel ActivePane { get; private set; }
public ObservableCollectionExtended<IStatusMessage> Log { get; } = new ObservableCollectionExtended<IStatusMessage>();
public readonly Lazy<CompilerVM> Compiler;
public readonly Lazy<InstallerVM> Installer;
public readonly Lazy<SettingsVM> SettingsPane;
public readonly Lazy<ModListGalleryVM> Gallery;
public readonly ModeSelectionVM ModeSelectionVM;
public readonly Lazy<ModListContentsVM> ModListContentsVM;
public readonly UserInterventionHandlers UserInterventionHandlers;
public ICommand CopyVersionCommand { get; }
public ICommand ShowLoginManagerVM { get; }
public ICommand OpenSettingsCommand { get; }
public string VersionDisplay { get; }
[Reactive]
public bool UpdateAvailable { get; private set; }
public MainWindowVM(MainWindow mainWindow, MainSettings settings)
{
ConverterRegistration.Register();
MainWindow = mainWindow;
Settings = settings;
Installer = new Lazy<InstallerVM>(() => new InstallerVM(this));
Compiler = new Lazy<CompilerVM>(() => new CompilerVM(this));
SettingsPane = new Lazy<SettingsVM>(() => new SettingsVM(this));
Gallery = new Lazy<ModListGalleryVM>(() => new ModListGalleryVM(this));
ModeSelectionVM = new ModeSelectionVM(this);
ModListContentsVM = new Lazy<ModListContentsVM>(() => new ModListContentsVM(this));
UserInterventionHandlers = new UserInterventionHandlers(this);
// Set up logging
Utils.LogMessages
.ObserveOn(RxApp.TaskpoolScheduler)
.ToObservableChangeSet()
.Buffer(TimeSpan.FromMilliseconds(250), RxApp.TaskpoolScheduler)
.Where(l => l.Count > 0)
.FlattenBufferResult()
.ObserveOnGuiThread()
.Bind(Log)
.Subscribe()
.DisposeWith(CompositeDisposable);
Utils.LogMessages
.Where(a => a is IUserIntervention or CriticalFailureIntervention)
.ObserveOnGuiThread()
.SelectTask(async msg =>
{
try
{
await UserInterventionHandlers.Handle(msg);
}
catch (Exception ex)
when (ex.GetType() != typeof(TaskCanceledException))
{
Utils.Error(ex, $"Error while handling user intervention of type {msg?.GetType()}");
try
{
if (msg is IUserIntervention {Handled: false} intervention)
{
intervention.Cancel();
}
}
catch (Exception cancelEx)
{
Utils.Error(cancelEx, $"Error while cancelling user intervention of type {msg?.GetType()}");
}
}
})
.Subscribe()
.DisposeWith(CompositeDisposable);
if (IsStartingFromModlist(out var path))
{
Installer.Value.ModListLocation.TargetPath = path;
NavigateTo(Installer.Value);
}
else
{
// Start on mode selection
NavigateTo(ModeSelectionVM);
}
try
{
var assembly = Assembly.GetExecutingAssembly();
var fvi = FileVersionInfo.GetVersionInfo(assembly.Location);
Consts.CurrentMinimumWabbajackVersion = Version.Parse(fvi.FileVersion);
VersionDisplay = $"v{fvi.FileVersion}";
Utils.Log($"Wabbajack Version: {fvi.FileVersion}");
Task.Run(() => Metrics.Send("started_wabbajack", fvi.FileVersion)).FireAndForget();
Task.Run(() => Metrics.Send("started_sha", ThisAssembly.Git.Sha));
}
catch (Exception ex)
{
Utils.Error(ex);
VersionDisplay = "ERROR";
}
CopyVersionCommand = ReactiveCommand.Create(() =>
{
Clipboard.SetText($"Wabbajack {VersionDisplay}\n{ThisAssembly.Git.Sha}");
});
OpenSettingsCommand = ReactiveCommand.Create(
canExecute: this.WhenAny(x => x.ActivePane)
.Select(active => !SettingsPane.IsValueCreated || !object.ReferenceEquals(active, SettingsPane.Value)),
execute: () => NavigateTo(SettingsPane.Value));
}
private static bool IsStartingFromModlist(out AbsolutePath modlistPath)
{
if (CLIArguments.InstallPath == null)
{
modlistPath = default;
return false;
}
modlistPath = (AbsolutePath)CLIArguments.InstallPath;
return true;
}
public void OpenInstaller(AbsolutePath path)
{
if (path == default) return;
var installer = Installer.Value;
Settings.Installer.LastInstalledListLocation = path;
NavigateTo(installer);
installer.ModListLocation.TargetPath = path;
}
public void NavigateTo(ViewModel vm)
{
ActivePane = vm;
}
public void NavigateTo<T>(T vm)
where T : ViewModel, IBackNavigatingVM
{
vm.NavigateBackTarget = ActivePane;
ActivePane = vm;
}
public async Task ShutdownApplication()
{
Dispose();
Settings.PosX = MainWindow.Left;
Settings.PosY = MainWindow.Top;
Settings.Width = MainWindow.Width;
Settings.Height = MainWindow.Height;
await MainSettings.SaveSettings(Settings);
Application.Current.Shutdown();
}
}
}

View File

@ -0,0 +1,78 @@
using System;
using System.Collections.ObjectModel;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Text.RegularExpressions;
using ReactiveUI.Fody.Helpers;
using Wabbajack.Lib;
using DynamicData;
using DynamicData.Binding;
namespace Wabbajack
{
public class ModListContentsVM : BackNavigatingVM
{
private MainWindowVM _mwvm;
[Reactive]
public string Name { get; set; }
[Reactive]
public ObservableCollection<DetailedStatusItem> Status { get; set; }
[Reactive]
public string SearchString { get; set; }
private readonly ReadOnlyObservableCollection<ModListArchive> _archives;
public ReadOnlyObservableCollection<ModListArchive> Archives => _archives;
private static readonly Regex NameMatcher = new(@"(?<=\.)[^\.]+(?=\+State)", RegexOptions.Compiled);
public ModListContentsVM(MainWindowVM mwvm) : base(mwvm)
{
_mwvm = mwvm;
Status = new ObservableCollectionExtended<DetailedStatusItem>();
string TransformClassName(Archive a)
{
var cname = a.State.GetType().FullName;
if (cname == null) return null;
var match = NameMatcher.Match(cname);
return match.Success ? match.ToString() : null;
}
this.Status
.ToObservableChangeSet()
.Transform(a => new ModListArchive
{
Name = a.Name,
Size = a.Archive?.Size ?? 0,
Url = a.Url ?? "",
Downloader = TransformClassName(a.Archive) ?? "Unknown",
Hash = a.Archive!.Hash.ToBase64()
})
.Filter(this.WhenAny(x => x.SearchString)
.StartWith("")
.Throttle(TimeSpan.FromMilliseconds(250))
.Select<string, Func<ModListArchive, bool>>(s => (ModListArchive ar) =>
string.IsNullOrEmpty(s) ||
ar.Name.ContainsCaseInsensitive(s) ||
ar.Downloader.ContainsCaseInsensitive(s) ||
ar.Hash.ContainsCaseInsensitive(s) ||
ar.Size.ToString() == s ||
ar.Url.ContainsCaseInsensitive(s)))
.ObserveOnGuiThread()
.Bind(out _archives)
.Subscribe()
.DisposeWith(CompositeDisposable);
}
}
public class ModListArchive
{
public string Name { get; set; }
public long Size { get; set; }
public string Url { get; set; }
public string Downloader { get; set; }
public string Hash { get; set; }
}
}

View File

@ -0,0 +1,117 @@
using ReactiveUI;
using System;
using System.IO;
using System.IO.Compression;
using System.Reactive;
using System.Reactive.Linq;
using System.Windows.Media.Imaging;
using Wabbajack.Common;
using Wabbajack.Lib;
using Wabbajack.Lib.ModListRegistry;
namespace Wabbajack
{
public class ModListVM : ViewModel
{
public ModList SourceModList { get; private set; }
public ModlistMetadata SourceModListMetadata { get; private set; }
public Exception Error { get; }
public AbsolutePath ModListPath { get; }
public string Name => SourceModList?.Name;
public string Readme => SourceModList?.Readme;
public string Author => SourceModList?.Author;
public string Description => SourceModList?.Description;
public Uri Website => SourceModList?.Website;
public ModManager ModManager => SourceModList?.ModManager ?? ModManager.MO2;
public Version Version => SourceModList?.Version;
public Version WabbajackVersion => SourceModList?.WabbajackVersion;
public bool IsNSFW => SourceModList?.IsNSFW ?? false;
// Image isn't exposed as a direct property, but as an observable.
// This acts as a caching mechanism, as interested parties will trigger it to be created,
// and the cached image will automatically be released when the last interested party is gone.
public IObservable<BitmapImage> ImageObservable { get; }
public ModListVM(AbsolutePath modListPath)
{
ModListPath = modListPath;
try
{
SourceModList = AInstaller.LoadFromFile(modListPath);
var metadataPath = modListPath.WithExtension(Consts.ModlistMetadataExtension);
if (metadataPath.Exists)
{
try
{
SourceModListMetadata = metadataPath.FromJson<ModlistMetadata>();
}
catch (Exception)
{
SourceModListMetadata = null;
}
}
}
catch (Exception ex)
{
Error = ex;
Utils.Error(ex, "Exception while loading the modlist!");
}
ImageObservable = Observable.Return(Unit.Default)
// Download and retrieve bytes on background thread
.ObserveOn(RxApp.TaskpoolScheduler)
.SelectAsync(async filePath =>
{
try
{
await using var fs = await ModListPath.OpenShared();
using var ar = new ZipArchive(fs, ZipArchiveMode.Read);
var ms = new MemoryStream();
var entry = ar.GetEntry("modlist-image.png");
if (entry == null) return default(MemoryStream);
await using var e = entry.Open();
e.CopyTo(ms);
return ms;
}
catch (Exception ex)
{
Utils.Error(ex, $"Exception while caching Mod List image {Name}");
return default(MemoryStream);
}
})
// Create Bitmap image on GUI thread
.ObserveOnGuiThread()
.Select(memStream =>
{
if (memStream == null) return default(BitmapImage);
try
{
return UIUtils.BitmapImageFromStream(memStream);
}
catch (Exception ex)
{
Utils.Error(ex, $"Exception while caching Mod List image {Name}");
return default(BitmapImage);
}
})
// If ever would return null, show WJ logo instead
.Select(x => x ?? ResourceLinks.WabbajackLogoNoText.Value)
.Replay(1)
.RefCount();
}
public void OpenReadme()
{
if (string.IsNullOrEmpty(Readme)) return;
Utils.OpenWebsite(new Uri(Readme));
}
public override void Dispose()
{
base.Dispose();
// Just drop reference explicitly, as it's large, so it can be GCed
// Even if someone is holding a stale reference to the VM
SourceModList = null;
}
}
}

View File

@ -0,0 +1,31 @@
using ReactiveUI;
using System;
using System.Reactive.Linq;
using System.Windows.Media.Imaging;
using Wabbajack.Common;
using Wabbajack.Lib;
using Wabbajack.Lib.Downloaders;
namespace Wabbajack
{
public class ModVM : ViewModel
{
public IMetaState State { get; }
// Image isn't exposed as a direct property, but as an observable.
// This acts as a caching mechanism, as interested parties will trigger it to be created,
// and the cached image will automatically be released when the last interested party is gone.
public IObservable<BitmapImage> ImageObservable { get; }
public ModVM(IMetaState state)
{
State = state;
ImageObservable = Observable.Return(State.ImageURL.ToString())
.ObserveOn(RxApp.TaskpoolScheduler)
.DownloadBitmapImage((ex) => Utils.Log($"Skipping slide for mod {State.Name}"))
.Replay(1)
.RefCount(TimeSpan.FromMilliseconds(5000));
}
}
}

View File

@ -0,0 +1,41 @@
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using System;
using System.IO;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Windows.Input;
using Wabbajack.Common;
using Wabbajack.Lib;
namespace Wabbajack
{
public class ModeSelectionVM : ViewModel
{
private MainWindowVM _mainVM;
public ICommand BrowseCommand { get; }
public ICommand InstallCommand { get; }
public ICommand CompileCommand { get; }
public ReactiveCommand<Unit, Unit> UpdateCommand { get; }
public ModeSelectionVM(MainWindowVM mainVM)
{
_mainVM = mainVM;
InstallCommand = ReactiveCommand.Create(
execute: () =>
{
var path = mainVM.Settings.Installer.LastInstalledListLocation;
if (path == default || !path.Exists)
{
path = UIUtils.OpenFileDialog($"*{Consts.ModListExtension}|*{Consts.ModListExtension}");
}
_mainVM.OpenInstaller(path);
});
CompileCommand = ReactiveCommand.Create(() => mainVM.NavigateTo(mainVM.Compiler.Value));
BrowseCommand = ReactiveCommand.Create(() => mainVM.NavigateTo(mainVM.Gallery.Value));
}
}
}

View File

@ -0,0 +1,83 @@
using System;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.Common;
using Wabbajack.Lib.AuthorApi;
using Wabbajack.Lib.FileUploader;
namespace Wabbajack
{
public class AuthorFilesVM : BackNavigatingVM
{
private readonly ObservableAsPropertyHelper<Visibility> _isVisible;
public Visibility IsVisible => _isVisible.Value;
public ICommand SelectFile { get; }
public ICommand HyperlinkCommand { get; }
public IReactiveCommand Upload { get; }
public IReactiveCommand ManageFiles { get; }
[Reactive] public double UploadProgress { get; set; }
[Reactive] public string FinalUrl { get; set; }
private WorkQueue Queue = new WorkQueue(1);
public FilePickerVM Picker { get;}
private Subject<bool> _isUploading = new Subject<bool>();
private IObservable<bool> IsUploading { get; }
public AuthorFilesVM(SettingsVM vm) : base(vm.MWVM)
{
IsUploading = _isUploading;
Picker = new FilePickerVM(this);
_isVisible = AuthorAPI.HaveAuthorAPIKey.Select(h => h ? Visibility.Visible : Visibility.Collapsed)
.ToProperty(this, x => x.IsVisible);
SelectFile = Picker.ConstructTypicalPickerCommand(IsUploading.StartWith(false).Select(u => !u));
HyperlinkCommand = ReactiveCommand.Create(() => Clipboard.SetText(FinalUrl));
ManageFiles = ReactiveCommand.Create(async () =>
{
var authorApiKey = await AuthorAPI.GetAPIKey();
Utils.OpenWebsite(new Uri($"{Consts.WabbajackBuildServerUri}author_controls/login/{authorApiKey}"));
});
Upload = ReactiveCommand.Create(async () =>
{
_isUploading.OnNext(true);
try
{
using var queue = new WorkQueue();
var result = await (await Client.Create()).UploadFile(queue, Picker.TargetPath,
(msg, progress) =>
{
FinalUrl = msg;
UploadProgress = (double)progress;
});
FinalUrl = result.ToString();
}
catch (Exception ex)
{
FinalUrl = ex.ToString();
}
finally
{
FinalUrl = FinalUrl.Replace(" ", "%20");
_isUploading.OnNext(false);
}
}, IsUploading.StartWith(false).Select(u => !u)
.CombineLatest(Picker.WhenAnyValue(t => t.TargetPath).Select(f => f != default),
(a, b) => a && b));
}
}
}

View File

@ -0,0 +1,99 @@
using System;
using System.Net.Mail;
using System.Reactive.Linq;
using System.Security;
using System.Threading.Tasks;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.Common;
using Wabbajack.Lib;
using Wabbajack.Lib.Downloaders;
namespace Wabbajack
{
public class CredentialsLoginVM : ViewModel
{
[Reactive]
public string Username { get; set; }
[Reactive]
public string MFAKey { get; set; }
[Reactive]
public LoginReturnMessage ReturnMessage { get; set; }
[Reactive]
public bool LoggingIn { get; private set; }
private readonly ObservableAsPropertyHelper<bool> _loginEnabled;
public bool LoginEnabled => _loginEnabled.Value;
private readonly ObservableAsPropertyHelper<bool> _mfaVisible;
public bool MFAVisible => _mfaVisible.Value;
private readonly INeedsLoginCredentials _downloader;
public CredentialsLoginVM(INeedsLoginCredentials downloader)
{
_downloader = downloader;
_loginEnabled = this.WhenAny(x => x.Username)
.Select(IsValidAddress)
.CombineLatest(
this.WhenAny(x => x.LoggingIn),
(valid, loggingIn) =>
{
return valid && !loggingIn;
})
.ToGuiProperty(this,
nameof(LoginEnabled));
_mfaVisible = this.WhenAny(x => x.ReturnMessage)
.Select(x => x.ReturnCode == LoginReturnCode.NeedsMFA)
.ToGuiProperty(this, nameof(MFAVisible));
}
public async Task Login(SecureString password)
{
try
{
LoggingIn = true;
if (password == null || password.Length == 0)
{
ReturnMessage = new LoginReturnMessage("You need to input a password!", LoginReturnCode.BadInput);
return;
}
ReturnMessage = await _downloader.LoginWithCredentials(Username, password, string.IsNullOrWhiteSpace(MFAKey) ? null : MFAKey);
password.Clear();
}
catch (Exception e)
{
Utils.Error(e, "Exception while trying to login");
ReturnMessage = new LoginReturnMessage($"Unhandled exception: {e.Message}", LoginReturnCode.InternalError);
}
finally
{
LoggingIn = false;
}
}
private static bool IsValidAddress(string s)
{
if (string.IsNullOrWhiteSpace(s))
return false;
try
{
var _ = new MailAddress(s);
}
catch (Exception)
{
return false;
}
return true;
}
}
}

View File

@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using Microsoft.WindowsAPICodePack.Shell.PropertySystem;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.Common;
using Wabbajack.Lib;
using Wabbajack.Lib.Downloaders;
namespace Wabbajack
{
public class LoginManagerVM : BackNavigatingVM
{
public List<LoginTargetVM> Downloaders { get; }
public LoginManagerVM(SettingsVM settingsVM)
: base(settingsVM.MWVM)
{
Downloaders = DownloadDispatcher.Downloaders
.OfType<INeedsLogin>()
.OrderBy(x => x.SiteName)
.Select(x => new LoginTargetVM(x))
.ToList();
}
}
public class LoginTargetVM : ViewModel
{
private readonly ObservableAsPropertyHelper<string> _metaInfo;
public string MetaInfo => _metaInfo.Value;
public INeedsLogin Login { get; }
public INeedsLoginCredentials LoginWithCredentials { get; }
public bool UsesCredentials { get; }
public ReactiveCommand<Unit, Unit> TriggerCredentialsLogin;
private ImageSource _favicon = null;
public ImageSource Favicon { get => _favicon; set => RaiseAndSetIfChanged(ref _favicon, value); }
public LoginTargetVM(INeedsLogin login)
{
Login = login;
LoadImage();
if (login is INeedsLoginCredentials loginWithCredentials)
{
UsesCredentials = true;
LoginWithCredentials = loginWithCredentials;
}
_metaInfo = (login.MetaInfo ?? Observable.Return(""))
.ToGuiProperty(this, nameof(MetaInfo));
if (!UsesCredentials)
return;
TriggerCredentialsLogin = ReactiveCommand.Create(() =>
{
if (!(login is INeedsLoginCredentials))
return;
var loginWindow = new LoginWindowView(LoginWithCredentials);
loginWindow.Show();
}, LoginWithCredentials.IsLoggedIn.Select(b => !b).ObserveOnGuiThread());
}
private void LoadImage()
{
Task.Run(async () =>
{
if (Login.IconUri == null) return;
if(!Consts.FaviconCacheFolderPath.Exists)
Consts.FaviconCacheFolderPath.CreateDirectory();
var faviconIcon = Consts.FaviconCacheFolderPath.Combine($"{Login.SiteName}.ico");
if (faviconIcon.Exists)
{
var fsi = new FileInfo(faviconIcon.ToString());
var creationDate = fsi.CreationTimeUtc;
var now = DateTime.UtcNow;
//delete favicons older than 10 days
if ((now - creationDate).TotalDays > 10)
await faviconIcon.DeleteAsync();
}
if (faviconIcon.Exists)
{
await using var fs = await faviconIcon.OpenRead();
var ms = new MemoryStream((int)fs.Length);
await fs.CopyToAsync(ms);
ms.Position = 0;
var source = new BitmapImage();
source.BeginInit();
source.StreamSource = ms;
source.EndInit();
source.Freeze();
Favicon = source;
}
else
{
using var img = await new Lib.Http.Client().GetAsync(Login.IconUri, errorsAsExceptions: false);
if (!img.IsSuccessStatusCode) return;
var icoData = new MemoryStream(await img.Content.ReadAsByteArrayAsync());
var data = new Icon(icoData);
var ms = new MemoryStream();
data.ToBitmap().Save(ms, ImageFormat.Png);
ms.Position = 0;
await using (var fs = await faviconIcon.Create())
{
await ms.CopyToAsync(fs);
ms.Position = 0;
}
var source = new BitmapImage();
source.BeginInit();
source.StreamSource = ms;
source.EndInit();
source.Freeze();
Favicon = source;
}
});
}
}
}

View File

@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using ReactiveUI;
using Wabbajack.Lib;
namespace Wabbajack
{
public class SettingsVM : BackNavigatingVM
{
public MainWindowVM MWVM { get; }
public LoginManagerVM Login { get; }
public PerformanceSettings Performance { get; }
public FiltersSettings Filters { get; }
public AuthorFilesVM AuthorFile { get; }
public ICommand OpenTerminalCommand { get; }
public SettingsVM(MainWindowVM mainWindowVM)
: base(mainWindowVM)
{
MWVM = mainWindowVM;
Login = new LoginManagerVM(this);
Performance = mainWindowVM.Settings.Performance;
AuthorFile = new AuthorFilesVM(this);
Filters = mainWindowVM.Settings.Filters;
OpenTerminalCommand = ReactiveCommand.CreateFromTask(() => OpenTerminal());
}
private async Task OpenTerminal()
{
var process = new ProcessStartInfo
{
FileName = "cmd.exe",
WorkingDirectory = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location)
};
Process.Start(process);
await MWVM.ShutdownApplication();
}
}
}

View File

@ -0,0 +1,161 @@
using DynamicData;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using System;
using System.Diagnostics;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Text.RegularExpressions;
using System.Windows.Media.Imaging;
using Wabbajack.Common;
using Wabbajack.Lib;
using Wabbajack.Lib.Downloaders;
namespace Wabbajack
{
public class SlideShow : ViewModel
{
private readonly Random _random = new Random();
public InstallerVM Installer { get; }
[Reactive]
public bool ShowNSFW { get; set; }
[Reactive]
public bool Enable { get; set; } = true;
private readonly ObservableAsPropertyHelper<BitmapImage> _image;
public BitmapImage Image => _image.Value;
private readonly ObservableAsPropertyHelper<ModVM> _targetMod;
public ModVM TargetMod => _targetMod.Value;
public ReactiveCommand<Unit, Unit> SlideShowNextItemCommand { get; } = ReactiveCommand.Create(() => { });
public ReactiveCommand<Unit, Unit> VisitURLCommand { get; }
public const int PreloadAmount = 4;
public SlideShow(InstallerVM appState)
{
Installer = appState;
// Wire target slideshow index
var intervalSeconds = 10;
// Compile all the sources that trigger a slideshow update, any of which trigger a counter update
var selectedIndex = Observable.Merge(
// If user requests one manually
SlideShowNextItemCommand.StartingExecution(),
// If the natural timer fires
Observable.Merge(
// Start with an initial timer
Observable.Return(Observable.Interval(TimeSpan.FromSeconds(intervalSeconds))),
// but reset timer if user requests one
SlideShowNextItemCommand.StartingExecution()
.Select(_ => Observable.Interval(TimeSpan.FromSeconds(intervalSeconds))))
// When a new timer comes in, swap to it
.Switch()
.Unit()
// Only subscribe to timer if enabled and installing
.FlowSwitch(
Observable.CombineLatest(
this.WhenAny(x => x.Enable),
this.WhenAny(x => x.Installer.Installing),
resultSelector: (enabled, installing) => enabled && installing)))
// When filter switch enabled, fire an initial signal
.StartWith(Unit.Default)
// Only subscribe to slideshow triggers if started
.FlowSwitch(this.WhenAny(x => x.Installer.StartedInstallation))
// Block spam
.Debounce(TimeSpan.FromMilliseconds(250), RxApp.MainThreadScheduler)
.Scan(
seed: 0,
accumulator: (i, _) => i + 1)
.Publish()
.RefCount();
// Dynamic list changeset of mod VMs to display
var modVMs = this.WhenAny(x => x.Installer.ModList)
// Whenever modlist changes, grab the list of its slides
.Select(modList =>
{
if (modList?.SourceModList?.Archives == null)
{
return Observable.Empty<IMetaState>()
.ToObservableChangeSet(x => x.URL);
}
return modList.SourceModList.Archives
.Select(m => m.State)
.OfType<IMetaState>()
.Where(x => x.URL != default && x.ImageURL != default)
.DistinctBy(x => x.URL)
// Shuffle it
.Shuffle(_random)
.AsObservableChangeSet(x => x.URL);
})
// Switch to the new list after every ModList change
.Switch()
.Transform(mod => new ModVM(mod))
.DisposeMany()
// Filter out any NSFW slides if we don't want them
.AutoRefreshOnObservable(slide => this.WhenAny(x => x.ShowNSFW))
.Filter(slide => !slide.State.IsNSFW || ShowNSFW)
.RefCount();
// Find target mod to display by combining dynamic list with currently desired index
_targetMod = Observable.CombineLatest(
modVMs.QueryWhenChanged(),
selectedIndex,
resultSelector: (query, selected) =>
{
var index = selected % (query.Count == 0 ? 1 : query.Count);
return query.Items.ElementAtOrDefault(index);
})
.StartWith(default(ModVM))
.ToGuiProperty(this, nameof(TargetMod));
// Mark interest and materialize image of target mod
_image = this.WhenAny(x => x.TargetMod)
// We want to Switch here, not SelectMany, as we want to hotswap to newest target without waiting on old ones
.Select(x => x?.ImageObservable ?? Observable.Return(default(BitmapImage)))
.Switch()
.ToGuiProperty(this, nameof(Image));
VisitURLCommand = ReactiveCommand.Create(
execute: () =>
{
Utils.OpenWebsite(TargetMod.State.URL);
return Unit.Default;
},
canExecute: this.WhenAny(x => x.TargetMod.State.URL)
.Select(x =>
{
//var regex = new Regex("^(http|https):\\/\\/");
var scheme = x?.Scheme;
return scheme != null &&
(scheme.Equals("https", StringComparison.OrdinalIgnoreCase) ||
scheme.Equals("http", StringComparison.OrdinalIgnoreCase));
})
.ObserveOnGuiThread());
// Preload upcoming images
var list = Observable.CombineLatest(
modVMs.QueryWhenChanged(),
selectedIndex,
resultSelector: (query, selected) =>
{
// Retrieve the mods that should be preloaded
var index = selected % (query.Count == 0 ? 1 : query.Count);
var amountToTake = Math.Min(query.Count - index, PreloadAmount);
return query.Items.Skip(index).Take(amountToTake).ToObservable();
})
.Select(i => i.ToObservableChangeSet())
.Switch()
.Transform(mod => mod.ImageObservable.Subscribe())
.DisposeMany()
.AsObservableList();
}
}
}

View File

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Wabbajack.Common;
using Wabbajack.Lib;
namespace Wabbajack
{
public class ConfirmUpdateOfExistingInstallVM : ViewModel, IUserIntervention
{
public ConfirmUpdateOfExistingInstall Source { get; }
public MO2InstallerVM Installer { get; }
public bool Handled => ((IUserIntervention)Source).Handled;
public int CpuID => ((IUserIntervention)Source).CpuID;
public DateTime Timestamp => ((IUserIntervention)Source).Timestamp;
public string ShortDescription => ((IUserIntervention)Source).ShortDescription;
public string ExtendedDescription => ((IUserIntervention)Source).ExtendedDescription;
public ConfirmUpdateOfExistingInstallVM(MO2InstallerVM installer, ConfirmUpdateOfExistingInstall confirm)
{
Source = confirm;
Installer = installer;
}
public void Cancel()
{
((IUserIntervention)Source).Cancel();
}
}
}

View File

@ -0,0 +1,211 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Windows;
using System.Windows.Threading;
using CefSharp;
using ReactiveUI;
using Wabbajack.Common;
using Wabbajack.Common.StatusFeed;
using Wabbajack.Lib;
using Wabbajack.Lib.Downloaders;
using Wabbajack.Lib.LibCefHelpers;
using Wabbajack.Lib.NexusApi;
using Wabbajack.Lib.WebAutomation;
using WebSocketSharp;
namespace Wabbajack
{
public class UserInterventionHandlers
{
public MainWindowVM MainWindow { get; }
private AsyncLock _browserLock = new();
public UserInterventionHandlers(MainWindowVM mvm)
{
MainWindow = mvm;
}
private async Task WrapBrowserJob(IUserIntervention intervention, Func<WebBrowserVM, CancellationTokenSource, Task> toDo)
{
var wait = await _browserLock.WaitAsync();
var cancel = new CancellationTokenSource();
var oldPane = MainWindow.ActivePane;
using var vm = await WebBrowserVM.GetNew();
MainWindow.NavigateTo(vm);
vm.BackCommand = ReactiveCommand.Create(() =>
{
cancel.Cancel();
MainWindow.NavigateTo(oldPane);
intervention.Cancel();
});
try
{
await toDo(vm, cancel);
}
catch (TaskCanceledException)
{
intervention.Cancel();
}
catch (Exception ex)
{
Utils.Error(ex);
intervention.Cancel();
}
finally
{
wait.Dispose();
}
MainWindow.NavigateTo(oldPane);
}
public async Task Handle(IStatusMessage msg)
{
switch (msg)
{
case RequestNexusAuthorization c:
await WrapBrowserJob(c, async (vm, cancel) =>
{
await vm.Driver.WaitForInitialized();
var key = await NexusApiClient.SetupNexusLogin(new CefSharpWrapper(vm.Browser), m => vm.Instructions = m, cancel.Token);
c.Resume(key);
});
break;
case ManuallyDownloadNexusFile c:
await WrapBrowserJob(c, (vm, cancel) => HandleManualNexusDownload(vm, cancel, c));
break;
case ManuallyDownloadFile c:
await WrapBrowserJob(c, (vm, cancel) => HandleManualDownload(vm, cancel, c));
break;
case AbstractNeedsLoginDownloader.RequestSiteLogin c:
await WrapBrowserJob(c, async (vm, cancel) =>
{
await vm.Driver.WaitForInitialized();
var data = await c.Downloader.GetAndCacheCookies(new CefSharpWrapper(vm.Browser), m => vm.Instructions = m, cancel.Token);
c.Resume(data);
});
break;
case RequestOAuthLogin oa:
await WrapBrowserJob(oa, async (vm, cancel) =>
{
await OAuthLogin(oa, vm, cancel);
});
break;
case CriticalFailureIntervention c:
MessageBox.Show(c.ExtendedDescription, c.ShortDescription, MessageBoxButton.OK,
MessageBoxImage.Error);
c.Cancel();
if (c.ExitApplication) await MainWindow.ShutdownApplication();
break;
case ConfirmationIntervention c:
break;
default:
throw new NotImplementedException($"No handler for {msg}");
}
}
private async Task OAuthLogin(RequestOAuthLogin oa, WebBrowserVM vm, CancellationTokenSource cancel)
{
await vm.Driver.WaitForInitialized();
vm.Instructions = $"Please log in and allow Wabbajack to access your {oa.SiteName} account";
var wrapper = new CefSharpWrapper(vm.Browser);
var scopes = string.Join(" ", oa.Scopes);
var state = Guid.NewGuid().ToString();
var oldHandler = Helpers.SchemeHandler;
Helpers.SchemeHandler = (browser, frame, _, request) =>
{
var req = new Uri(request.Url);
Utils.LogStraightToFile($"Got Scheme callback {req}");
var parsed = HttpUtility.ParseQueryString(req.Query);
if (parsed.Contains("state"))
{
if (parsed.Get("state") != state)
{
Utils.Log("Bad OAuth state, state, this shouldn't happen");
oa.Cancel();
return new ResourceHandler();
}
}
if (parsed.Contains("code"))
{
Helpers.SchemeHandler = oldHandler;
oa.Resume(parsed.Get("code"));
}
else
{
oa.Cancel();
}
return new ResourceHandler();
};
await wrapper.NavigateTo(new Uri(oa.AuthorizationEndpoint + $"?response_type=code&client_id={oa.ClientID}&state={state}&scope={scopes}"));
while (!oa.Task.IsCanceled && !oa.Task.IsCompleted && !cancel.IsCancellationRequested)
await Task.Delay(250);
}
private async Task HandleManualDownload(WebBrowserVM vm, CancellationTokenSource cancel, ManuallyDownloadFile manuallyDownloadFile)
{
var browser = new CefSharpWrapper(vm.Browser);
vm.Instructions = $"Please locate and download {manuallyDownloadFile.State.Url}";
var result = new TaskCompletionSource<Uri>();
browser.DownloadHandler = uri =>
{
//var client = Helpers.GetClient(browser.GetCookies("").Result, browser.Location);
result.SetResult(uri);
};
await vm.Driver.WaitForInitialized();
await browser.NavigateTo(new Uri(manuallyDownloadFile.State.Url));
while (!cancel.IsCancellationRequested)
{
if (result.Task.IsCompleted)
{
var cookies = await Helpers.GetCookies();
var referer = browser.Location;
var client = Helpers.GetClient(cookies, referer);
manuallyDownloadFile.Resume(result.Task.Result, client);
break;
}
await Task.Delay(100);
}
}
private async Task HandleManualNexusDownload(WebBrowserVM vm, CancellationTokenSource cancel, ManuallyDownloadNexusFile manuallyDownloadNexusFile)
{
var state = manuallyDownloadNexusFile.State;
var game = state.Game.MetaData();
await vm.Driver.WaitForInitialized();
IWebDriver browser = new CefSharpWrapper(vm.Browser);
vm.Instructions = $"Click the download button to continue (get a NexusMods.com Premium account to automate this)";
browser.DownloadHandler = uri =>
{
manuallyDownloadNexusFile.Resume(uri);
browser.DownloadHandler = null;
};
var url = new Uri(@$"https://www.nexusmods.com/{game.NexusName}/mods/{state.ModID}?tab=files&file_id={state.FileID}");
await browser.NavigateTo(url);
while (!cancel.IsCancellationRequested && !manuallyDownloadNexusFile.Task.IsCompleted) {
await Task.Delay(250);
}
}
}
}

View File

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Text;
using System.Threading.Tasks;
using CefSharp;
using CefSharp.Wpf;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.Lib;
using Wabbajack.Lib.LibCefHelpers;
using Wabbajack.Lib.WebAutomation;
namespace Wabbajack
{
public class WebBrowserVM : ViewModel, IBackNavigatingVM, IDisposable
{
[Reactive]
public string Instructions { get; set; }
public IWebBrowser Browser { get; } = new ChromiumWebBrowser();
public CefSharpWrapper Driver => new CefSharpWrapper(Browser);
[Reactive]
public ViewModel NavigateBackTarget { get; set; }
[Reactive]
public ReactiveCommand<Unit, Unit> BackCommand { get; set; }
public Subject<bool> IsBackEnabledSubject { get; } = new Subject<bool>();
public IObservable<bool> IsBackEnabled { get; }
private WebBrowserVM(string url = "http://www.wabbajack.org")
{
IsBackEnabled = IsBackEnabledSubject.StartWith(true);
Instructions = "Wabbajack Web Browser";
}
public static async Task<WebBrowserVM> GetNew(string url = "http://www.wabbajack.org")
{
// Make sure libraries are extracted first
return new WebBrowserVM(url);
}
public override void Dispose()
{
Browser.Dispose();
base.Dispose();
}
}
}

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Wabbajack
{
/// <summary>
/// Interaction logic for AttentionBorder.xaml
/// </summary>
public partial class AttentionBorder : UserControl
{
public bool Failure
{
get => (bool)GetValue(FailureProperty);
set => SetValue(FailureProperty, value);
}
public static readonly DependencyProperty FailureProperty = DependencyProperty.Register(nameof(Failure), typeof(bool), typeof(AttentionBorder),
new FrameworkPropertyMetadata(default(bool)));
}
}

View File

@ -0,0 +1,85 @@
<UserControl
x:Class="Wabbajack.BeginButton"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:icon="http://metro.mahapps.com/winfx/xaml/iconpacks"
xmlns:local="clr-namespace:Wabbajack"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid>
<Border
x:Name="BeginButtonPurpleGlow"
Width="76"
Height="76"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="{StaticResource PrimaryVariantBrush}"
CornerRadius="43"
Visibility="{Binding IsEnabled, ElementName=button, Converter={StaticResource bool2VisibilityHiddenConverter}}">
<Border.Effect>
<BlurEffect Radius="10" />
</Border.Effect>
<Border.Style>
<Style TargetType="Border">
<Setter Property="Opacity" Value="0.5" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsMouseOver, ElementName=BeginButton}" Value="True">
<Setter Property="Opacity" Value="0.8" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
<Button
x:Name="button"
Width="54"
Height="54"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Command="{Binding Command, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}">
<icon:PackIconMaterial
Width="25"
Height="25"
Margin="5,0,0,0"
Kind="Play">
<icon:PackIconMaterial.Style>
<Style TargetType="icon:PackIconMaterial">
<Setter Property="Foreground" Value="#666666" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsEnabled, RelativeSource={RelativeSource AncestorType={x:Type Button}}}" Value="True">
<Setter Property="Foreground" Value="{StaticResource SecondaryBrush}" />
</DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsMouseOver, RelativeSource={RelativeSource AncestorType={x:Type Button}}}" Value="True" />
<Condition Binding="{Binding IsEnabled, RelativeSource={RelativeSource AncestorType={x:Type Button}}}" Value="True" />
</MultiDataTrigger.Conditions>
<MultiDataTrigger.Setters>
<Setter Property="Foreground" Value="{StaticResource IntenseSecondaryBrush}" />
</MultiDataTrigger.Setters>
</MultiDataTrigger>
</Style.Triggers>
</Style>
</icon:PackIconMaterial.Style>
</icon:PackIconMaterial>
<Button.Style>
<Style BasedOn="{StaticResource CircleButtonStyle}" TargetType="Button">
<Style.Triggers>
<DataTrigger Binding="{Binding IsEnabled, RelativeSource={RelativeSource Self}}" Value="True">
<Setter Property="BorderBrush" Value="{StaticResource SecondaryBrush}" />
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect
BlurRadius="15"
ShadowDepth="0"
Color="{StaticResource Secondary}" />
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</Grid>
</UserControl>

View File

@ -0,0 +1,25 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace Wabbajack
{
/// <summary>
/// Interaction logic for BeginButton.xaml
/// </summary>
public partial class BeginButton : UserControl
{
public ICommand Command
{
get => (ICommand)GetValue(CommandProperty);
set => SetValue(CommandProperty, value);
}
public static readonly DependencyProperty CommandProperty = DependencyProperty.Register(nameof(Command), typeof(ICommand), typeof(BeginButton),
new FrameworkPropertyMetadata(default(ICommand)));
public BeginButton()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,35 @@
<rxui:ReactiveUserControl
x:Class="Wabbajack.CpuLineView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Wabbajack"
xmlns:mahapps="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:rxui="http://reactiveui.net"
d:DesignHeight="65"
d:DesignWidth="400"
x:TypeArguments="local:CPUDisplayVM"
mc:Ignorable="d">
<Grid Background="{StaticResource WindowBackgroundBrush}">
<mahapps:MetroProgressBar
x:Name="BackgroundProgressBar"
Background="Transparent"
BorderThickness="0"
Foreground="{StaticResource DarkPrimaryVariantBrush}"
Maximum="1" />
<Grid Height="1" VerticalAlignment="Bottom">
<mahapps:MetroProgressBar
x:Name="ThinProgressBar"
Background="Transparent"
BorderThickness="0"
Foreground="{StaticResource DarkSecondaryBrush}"
Maximum="1" />
</Grid>
<TextBlock
x:Name="Text"
Margin="0,0,0,2"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
</Grid>
</rxui:ReactiveUserControl>

View File

@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using ReactiveUI;
namespace Wabbajack
{
/// <summary>
/// Interaction logic for CpuLineView.xaml
/// </summary>
public partial class CpuLineView : ReactiveUserControl<CPUDisplayVM>
{
public CpuLineView()
{
InitializeComponent();
this.WhenActivated(dispose =>
{
this.WhenAny(x => x.ViewModel.ProgressPercent)
.Select(x => x.Value)
.BindToStrict(this, x => x.BackgroundProgressBar.Value)
.DisposeWith(dispose);
this.WhenAny(x => x.ViewModel.ProgressPercent)
.Select(x => x.Value)
.BindToStrict(this, x => x.BackgroundProgressBar.Opacity)
.DisposeWith(dispose);
this.WhenAny(x => x.ViewModel.ProgressPercent)
.Select(x => x.Value)
.BindToStrict(this, x => x.ThinProgressBar.Value)
.DisposeWith(dispose);
this.WhenAny(x => x.ViewModel.Msg)
.BindToStrict(this, x => x.Text.Text)
.DisposeWith(dispose);
this.WhenAny(x => x.ViewModel.Msg)
.BindToStrict(this, x => x.Text.ToolTip)
.DisposeWith(dispose);
});
}
}
}

View File

@ -0,0 +1,32 @@
<local:UserControlRx
x:Class="Wabbajack.CpuView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Wabbajack"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="450"
d:DesignWidth="800"
x:TypeArguments="local:ICpuStatusVM"
mc:Ignorable="d">
<Grid x:Name="ControlGrid" Background="Transparent">
<Rectangle x:Name="HeatedBorderRect" Fill="{StaticResource HeatedBorderBrush}" />
<Border BorderBrush="Transparent" BorderThickness="1" />
<Grid Margin="1" Background="{StaticResource DarkBackgroundBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox Grid.Row="0"
x:Name="CpuListControl"
HorizontalContentAlignment="Stretch"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate>
<local:CpuLineView ViewModel="{Binding}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Grid>
</local:UserControlRx>

View File

@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.Lib;
using System.Windows.Controls.Primitives;
using System.Reactive.Linq;
using Wabbajack.Common;
namespace Wabbajack
{
/// <summary>
/// Interaction logic for CpuView.xaml
/// </summary>
public partial class CpuView : UserControlRx<ICpuStatusVM>
{
public Percent ProgressPercent
{
get => (Percent)GetValue(ProgressPercentProperty);
set => SetValue(ProgressPercentProperty, value);
}
public static readonly DependencyProperty ProgressPercentProperty = DependencyProperty.Register(nameof(ProgressPercent), typeof(Percent), typeof(CpuView),
new FrameworkPropertyMetadata(default(Percent), WireNotifyPropertyChanged));
public MainSettings SettingsHook
{
get => (MainSettings)GetValue(SettingsHookProperty);
set => SetValue(SettingsHookProperty, value);
}
public static readonly DependencyProperty SettingsHookProperty = DependencyProperty.Register(nameof(SettingsHook), typeof(MainSettings), typeof(CpuView),
new FrameworkPropertyMetadata(default(SettingsVM), WireNotifyPropertyChanged));
private bool _ShowingSettings;
public bool ShowingSettings { get => _ShowingSettings; set => this.RaiseAndSetIfChanged(ref _ShowingSettings, value); }
public CpuView()
{
InitializeComponent();
this.WhenActivated(disposable =>
{
this.WhenAny(x => x.ViewModel.StatusList)
.BindToStrict(this, x => x.CpuListControl.ItemsSource)
.DisposeWith(disposable);
// Progress
this.WhenAny(x => x.ProgressPercent)
.Select(p => p.Value)
.BindToStrict(this, x => x.HeatedBorderRect.Opacity)
.DisposeWith(disposable);
});
}
}
}

View File

@ -0,0 +1,163 @@
<local:UserControlRx
x:Class="Wabbajack.DetailImageView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:icon="http://metro.mahapps.com/winfx/xaml/iconpacks"
xmlns:lib="clr-namespace:Wabbajack.Lib;assembly=Wabbajack.Lib"
xmlns:local="clr-namespace:Wabbajack"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="450"
d:DesignWidth="800"
x:TypeArguments="lib:ViewModel"
ClipToBounds="True"
mc:Ignorable="d">
<UserControl.Resources>
<Color x:Key="TextBackgroundFill">#92000000</Color>
<SolidColorBrush x:Key="TextBackgroundFillBrush" Color="{StaticResource TextBackgroundFill}" />
<Color x:Key="TextBackgroundHoverFill">#DF000000</Color>
<Style x:Key="BackgroundBlurStyle" TargetType="TextBlock">
<Setter Property="Background" Value="{StaticResource TextBackgroundFillBrush}" />
<Setter Property="Foreground" Value="Transparent" />
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsMouseOver, ElementName=TextHoverTrigger}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="(TextBlock.Background).(SolidColorBrush.Color)"
To="{StaticResource TextBackgroundHoverFill}"
Duration="0:0:0.06" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="(TextBlock.Background).(SolidColorBrush.Color)"
To="{StaticResource TextBackgroundFill}"
Duration="0:0:0.06" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="5*" />
<ColumnDefinition Width="3*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Rectangle Grid.Row="0" Grid.RowSpan="4" Grid.Column="0" Grid.ColumnSpan="2"
Fill="{StaticResource WindowBackgroundBrush}" />
<Viewbox Grid.Row="0" Grid.RowSpan="4" Grid.Column="0" Grid.ColumnSpan="2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stretch="UniformToFill">
<Image x:Name="ModlistImage" />
</Viewbox>
<Image Grid.Row="0" Grid.RowSpan="4" Grid.Column="0" Grid.ColumnSpan="2"
x:Name="BadgeImage"
Width="60"
Height="60"
HorizontalAlignment="Left"
VerticalAlignment="Top" />
<TextBlock Grid.Row="2" Grid.Column="0"
x:Name="TitleTextShadow"
Margin="-20,5,-20,-20"
Padding="40,20"
HorizontalAlignment="Left"
VerticalAlignment="Bottom"
FontFamily="Lucida Sans"
FontSize="65"
FontWeight="Bold"
Style="{StaticResource BackgroundBlurStyle}"
TextWrapping="WrapWithOverflow">
<TextBlock.Effect>
<BlurEffect Radius="85" />
</TextBlock.Effect>
</TextBlock>
<TextBlock Grid.Row="3" Grid.Column="0"
x:Name="AuthorTextShadow"
Margin="35,-10,-10,10"
Padding="30,10"
HorizontalAlignment="Left"
FontFamily="Lucida Sans"
FontSize="30"
FontWeight="Bold"
Style="{StaticResource BackgroundBlurStyle}"
TextWrapping="WrapWithOverflow">
<TextBlock.Effect>
<BlurEffect Radius="55" />
</TextBlock.Effect>
<Run FontSize="15" Text="by" />
<Run x:Name="AuthorShadowTextRun" />
</TextBlock>
<TextBlock Grid.Row="2" Grid.Column="0"
x:Name="TitleTextBlock"
Margin="20,25,20,0"
HorizontalAlignment="Left"
VerticalAlignment="Bottom"
FontFamily="Lucida Sans"
FontSize="65"
FontWeight="Bold"
TextWrapping="WrapWithOverflow">
<TextBlock.Effect>
<DropShadowEffect />
</TextBlock.Effect>
</TextBlock>
<TextBlock Grid.Row="3" Grid.Column="0"
x:Name="AuthorTextBlock"
Margin="55,0,20,20"
FontFamily="Lucida Sans"
FontSize="30"
FontWeight="Bold"
TextWrapping="Wrap">
<TextBlock.Effect>
<DropShadowEffect />
</TextBlock.Effect>
<Run FontSize="15" Text="by" />
<Run x:Name="AuthorTextRun" />
</TextBlock>
<TextBlock Grid.Row="2" Grid.RowSpan="2" Grid.Column="1"
x:Name="DescriptionTextShadow"
Margin="-10,15,-5,15"
Padding="30,10"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
FontFamily="Lucida Sans"
FontSize="16"
Style="{StaticResource BackgroundBlurStyle}"
TextAlignment="Right"
TextWrapping="Wrap">
<TextBlock.Effect>
<BlurEffect Radius="55" />
</TextBlock.Effect>
</TextBlock>
<TextBlock Grid.Row="2" Grid.RowSpan="2" Grid.Column="1"
x:Name="DescriptionTextBlock"
Margin="20,25,25,25"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
FontFamily="Lucida Sans"
FontSize="16"
TextAlignment="Right"
TextWrapping="Wrap">
<TextBlock.Effect>
<DropShadowEffect />
</TextBlock.Effect>
</TextBlock>
<Rectangle Grid.Row="2" Grid.RowSpan="2" Grid.Column="0" Grid.ColumnSpan="2"
x:Name="TextHoverTrigger"
Fill="Transparent" />
</Grid>
</local:UserControlRx>

View File

@ -0,0 +1,131 @@
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using System;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Media;
using Wabbajack.Lib;
namespace Wabbajack
{
/// <summary>
/// Interaction logic for DetailImageView.xaml
/// </summary>
public partial class DetailImageView : UserControlRx<ViewModel>
{
public ImageSource Image
{
get => (ImageSource)GetValue(ImageProperty);
set => SetValue(ImageProperty, value);
}
public static readonly DependencyProperty ImageProperty = DependencyProperty.Register(nameof(Image), typeof(ImageSource), typeof(DetailImageView),
new FrameworkPropertyMetadata(default(ImageSource), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, WireNotifyPropertyChanged));
public ImageSource Badge
{
get => (ImageSource)GetValue(BadgeProperty);
set => SetValue(BadgeProperty, value);
}
public static readonly DependencyProperty BadgeProperty = DependencyProperty.Register(nameof(Badge), typeof(ImageSource), typeof(DetailImageView),
new FrameworkPropertyMetadata(default(ImageSource), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, WireNotifyPropertyChanged));
public string Title
{
get => (string)GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
public static readonly DependencyProperty TitleProperty = DependencyProperty.Register(nameof(Title), typeof(string), typeof(DetailImageView),
new FrameworkPropertyMetadata(default(string), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, WireNotifyPropertyChanged));
public string Author
{
get => (string)GetValue(AuthorProperty);
set => SetValue(AuthorProperty, value);
}
public static readonly DependencyProperty AuthorProperty = DependencyProperty.Register(nameof(Author), typeof(string), typeof(DetailImageView),
new FrameworkPropertyMetadata(default(string), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, WireNotifyPropertyChanged));
public string Description
{
get => (string)GetValue(DescriptionProperty);
set => SetValue(DescriptionProperty, value);
}
public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register(nameof(Description), typeof(string), typeof(DetailImageView),
new FrameworkPropertyMetadata(default(string), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, WireNotifyPropertyChanged));
public DetailImageView()
{
InitializeComponent();
this.WhenActivated(dispose =>
{
// Update textboxes
var authorVisible = this.WhenAny(x => x.Author)
.Select(x => string.IsNullOrWhiteSpace(x) ? Visibility.Collapsed : Visibility.Visible)
.Replay(1)
.RefCount();
authorVisible
.BindToStrict(this, x => x.AuthorTextBlock.Visibility)
.DisposeWith(dispose);
authorVisible
.BindToStrict(this, x => x.AuthorTextShadow.Visibility)
.DisposeWith(dispose);
this.WhenAny(x => x.Author)
.BindToStrict(this, x => x.AuthorTextRun.Text)
.DisposeWith(dispose);
this.WhenAny(x => x.Author)
.BindToStrict(this, x => x.AuthorShadowTextRun.Text)
.DisposeWith(dispose);
var descVisible = this.WhenAny(x => x.Description)
.Select(x => string.IsNullOrWhiteSpace(x) ? Visibility.Collapsed : Visibility.Visible)
.Replay(1)
.RefCount();
descVisible
.BindToStrict(this, x => x.DescriptionTextBlock.Visibility)
.DisposeWith(dispose);
descVisible
.BindToStrict(this, x => x.DescriptionTextShadow.Visibility)
.DisposeWith(dispose);
this.WhenAny(x => x.Description)
.BindToStrict(this, x => x.DescriptionTextBlock.Text)
.DisposeWith(dispose);
this.WhenAny(x => x.Description)
.BindToStrict(this, x => x.DescriptionTextShadow.Text)
.DisposeWith(dispose);
var titleVisible = this.WhenAny(x => x.Title)
.Select(x => string.IsNullOrWhiteSpace(x) ? Visibility.Collapsed : Visibility.Visible)
.Replay(1)
.RefCount();
titleVisible
.BindToStrict(this, x => x.TitleTextBlock.Visibility)
.DisposeWith(dispose);
titleVisible
.BindToStrict(this, x => x.TitleTextShadow.Visibility)
.DisposeWith(dispose);
this.WhenAny(x => x.Title)
.BindToStrict(this, x => x.TitleTextBlock.Text)
.DisposeWith(dispose);
this.WhenAny(x => x.Title)
.BindToStrict(this, x => x.TitleTextShadow.Text)
.DisposeWith(dispose);
// Update other items
this.WhenAny(x => x.Badge)
.BindToStrict(this, x => x.BadgeImage.Source)
.DisposeWith(dispose);
this.WhenAny(x => x.Image)
.BindToStrict(this, x => x.ModlistImage.Source)
.DisposeWith(dispose);
this.WhenAny(x => x.Image)
.Select(img => img == null ? Visibility.Hidden : Visibility.Visible)
.BindToStrict(this, x => x.Visibility)
.DisposeWith(dispose);
});
}
}
}

View File

@ -0,0 +1,156 @@
<UserControl
x:Class="Wabbajack.FilePicker"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:icon="http://metro.mahapps.com/winfx/xaml/iconpacks"
xmlns:local="clr-namespace:Wabbajack"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="35"
d:DesignWidth="400"
BorderBrush="{StaticResource DarkBackgroundBrush}"
mc:Ignorable="d">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="36" />
</Grid.ColumnDefinitions>
<Rectangle Grid.Column="0" Grid.ColumnSpan="2"
x:Name="ErrorGlow"
Fill="{StaticResource WarningBrush}"
Opacity="0.3"
Visibility="{Binding PickerVM.InError, Converter={StaticResource bool2VisibilityConverter}, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}">
<Rectangle.Effect>
<BlurEffect Radius="15" />
</Rectangle.Effect>
<Rectangle.Style>
<Style TargetType="Rectangle">
<Style.Triggers>
<DataTrigger Binding="{Binding PickerVM.InError, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
AutoReverse="True"
RepeatBehavior="Forever"
Storyboard.TargetProperty="(Rectangle.Fill).(SolidColorBrush.Color)"
To="{StaticResource YellowDark}"
Duration="0:0:0.8" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Rectangle.Style>
</Rectangle>
<Border Grid.Column="0" Grid.ColumnSpan="2"
x:Name="BackgroundCornerFillIn"
Margin="0,0,5,0"
Background="{StaticResource TextBoxBackground}"
CornerRadius="3" />
<TextBox Grid.Column="0"
Margin="5,1,-2,1"
VerticalContentAlignment="Center"
Background="{StaticResource DarkBackgroundBrush}"
Text="{Binding PickerVM.TargetPath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}, Converter={StaticResource AbsolutePathToStringConverter}}"
Visibility="{Binding PickerVM.ShowTextBoxInput, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" />
<Grid Grid.Column="1"
HorizontalAlignment="Right"
ClipToBounds="True">
<Border
Margin="3,1,0,1"
HorizontalAlignment="Right"
Background="{StaticResource WarningBrush}"
CornerRadius="3"
ToolTip="{Binding PickerVM.ErrorTooltip, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Width" Value="25" />
<Style.Triggers>
<DataTrigger Binding="{Binding PickerVM.InError, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Width"
To="33"
Duration="0:0:0.1">
<DoubleAnimation.EasingFunction>
<ExponentialEase EasingMode="EaseOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="Width"
To="25"
Duration="0:0:0.1">
<DoubleAnimation.EasingFunction>
<ExponentialEase EasingMode="EaseOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
<Border
Width="30"
HorizontalAlignment="Left"
Background="{StaticResource TextBoxBackground}"
CornerRadius="3">
<Button Command="{Binding PickerVM.SetTargetPathCommand, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" ToolTip="Set target path">
<icon:PackIconMaterial
Width="16"
Height="12"
Margin="4"
Kind="DotsHorizontal" />
</Button>
<Border.Effect>
<DropShadowEffect
BlurRadius="3"
Direction="0"
Opacity="0.5"
ShadowDepth="2" />
</Border.Effect>
</Border>
</Grid>
<Border Grid.Column="0" Grid.ColumnSpan="2"
x:Name="ErrorBorder"
Background="Transparent"
BorderBrush="{StaticResource WarningBrush}"
BorderThickness="1"
CornerRadius="3"
IsHitTestVisible="False"
Visibility="{Binding PickerVM.InError, Converter={StaticResource bool2VisibilityConverter}, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}">
<Border.Style>
<Style TargetType="Border">
<Style.Triggers>
<DataTrigger Binding="{Binding PickerVM.InError, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
AutoReverse="True"
RepeatBehavior="Forever"
Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)"
To="{StaticResource YellowDark}"
Duration="0:0:0.8" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
</Grid>
</UserControl>

View File

@ -0,0 +1,27 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using Wabbajack.Lib;
namespace Wabbajack
{
/// <summary>
/// Interaction logic for FilePicker.xaml
/// </summary>
public partial class FilePicker
{
// This exists, as utilizing the datacontext directly seemed to bug out the exit animations
// "Bouncing" off this property seems to fix it, though. Could perhaps be done other ways.
public FilePickerVM PickerVM
{
get => (FilePickerVM)GetValue(PickerVMProperty);
set => SetValue(PickerVMProperty, value);
}
public static readonly DependencyProperty PickerVMProperty = DependencyProperty.Register(nameof(PickerVM), typeof(FilePickerVM), typeof(FilePicker),
new FrameworkPropertyMetadata(default(FilePickerVM)));
public FilePicker()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,29 @@
<UserControl
x:Class="Wabbajack.HeatedBackgroundView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Wabbajack"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<Grid>
<Rectangle>
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="#191919" />
<GradientStop Offset="0.4" Color="#00191919" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Opacity="{Binding PercentCompleted, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}">
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0.2" Color="{StaticResource PrimaryVariant}" />
<GradientStop Offset="1" Color="{StaticResource WindowBackgroundColor}" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
</Grid>
</UserControl>

View File

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Wabbajack
{
/// <summary>
/// Interaction logic for HeatedBackgroundView.xaml
/// </summary>
public partial class HeatedBackgroundView : UserControl
{
public double PercentCompleted
{
get => (double)GetValue(PercentCompletedProperty);
set => SetValue(PercentCompletedProperty, value);
}
public static readonly DependencyProperty PercentCompletedProperty = DependencyProperty.Register(nameof(PercentCompleted), typeof(double), typeof(HeatedBackgroundView),
new FrameworkPropertyMetadata(default(double)));
public HeatedBackgroundView()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,26 @@
<UserControl
x:Class="Wabbajack.LogView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Wabbajack"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<Grid>
<Rectangle Fill="{StaticResource HeatedBorderBrush}" Opacity="{Binding ProgressPercent, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" />
<ListBox
local:AutoScrollBehavior.ScrollOnNewItem="True"
BorderBrush="Transparent"
BorderThickness="1"
ItemsSource="{Binding Log}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ShortDescription}" TextWrapping="WrapWithOverflow" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</UserControl>

View File

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Wabbajack
{
/// <summary>
/// Interaction logic for LogView.xaml
/// </summary>
public partial class LogView : UserControl
{
public double ProgressPercent
{
get => (double)GetValue(ProgressPercentProperty);
set => SetValue(ProgressPercentProperty, value);
}
public static readonly DependencyProperty ProgressPercentProperty = DependencyProperty.Register(nameof(ProgressPercent), typeof(double), typeof(LogView),
new FrameworkPropertyMetadata(default(double)));
public LogView()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,71 @@
<UserControl
x:Class="Wabbajack.ImageRadioButtonView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Wabbajack"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="35"
ClipToBounds="False"
mc:Ignorable="d">
<Grid>
<Border
x:Name="HoverOverPrimaryGlow"
Margin="-10"
Background="{StaticResource PrimaryVariantBrush}"
CornerRadius="15"
Opacity="0.3"
Visibility="{Binding IsChecked, Converter={StaticResource bool2VisibilityHiddenConverter}, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}">
<Border.Effect>
<BlurEffect Radius="15" />
</Border.Effect>
</Border>
<Border
x:Name="SelectedSecondaryGlow"
Margin="-2"
Background="{StaticResource SecondaryBrush}"
CornerRadius="12"
Opacity="0.3"
Visibility="{Binding IsChecked, Converter={StaticResource bool2VisibilityHiddenConverter}, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}">
<Border.Effect>
<BlurEffect Radius="10" />
</Border.Effect>
</Border>
<Button
Background="Transparent"
BorderThickness="0"
Click="Button_Click">
<Button.Template>
<ControlTemplate>
<Image Source="{Binding Image, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}">
<Image.Style>
<Style TargetType="Image">
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsMouseOver, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" Value="False" />
<Condition Binding="{Binding IsChecked, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" Value="False" />
</MultiDataTrigger.Conditions>
<MultiDataTrigger.Setters>
<Setter Property="Effect">
<Setter.Value>
<BlurEffect Radius="2" />
</Setter.Value>
</Setter>
</MultiDataTrigger.Setters>
</MultiDataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
</ControlTemplate>
</Button.Template>
</Button>
<Border
BorderBrush="{StaticResource SecondaryBrush}"
BorderThickness="1"
CornerRadius="9"
Opacity="0.8"
Visibility="{Binding IsChecked, Converter={StaticResource bool2VisibilityHiddenConverter}, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" />
</Grid>
</UserControl>

View File

@ -0,0 +1,47 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media.Imaging;
namespace Wabbajack
{
/// <summary>
/// Interaction logic for ImageRadioButtonView.xaml
/// </summary>
public partial class ImageRadioButtonView : UserControl
{
public bool IsChecked
{
get => (bool)GetValue(IsCheckedProperty);
set => SetValue(IsCheckedProperty, value);
}
public static readonly DependencyProperty IsCheckedProperty = DependencyProperty.Register(nameof(IsChecked), typeof(bool), typeof(ImageRadioButtonView),
new FrameworkPropertyMetadata(default(bool), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public BitmapImage Image
{
get => (BitmapImage)GetValue(ImageProperty);
set => SetValue(ImageProperty, value);
}
public static readonly DependencyProperty ImageProperty = DependencyProperty.Register(nameof(Image), typeof(BitmapImage), typeof(ImageRadioButtonView),
new FrameworkPropertyMetadata(default(BitmapImage)));
public ICommand Command
{
get => (ICommand)GetValue(CommandProperty);
set => SetValue(CommandProperty, value);
}
public static readonly DependencyProperty CommandProperty = DependencyProperty.Register(nameof(Command), typeof(ICommand), typeof(ImageRadioButtonView),
new FrameworkPropertyMetadata(default(ICommand)));
public ImageRadioButtonView()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
IsChecked = true;
}
}
}

View File

@ -0,0 +1,147 @@
<local:UserControlRx
x:Class="Wabbajack.TopProgressView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:lib="clr-namespace:Wabbajack.Lib;assembly=Wabbajack.Lib"
xmlns:local="clr-namespace:Wabbajack"
xmlns:mahapps="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="450"
d:DesignWidth="800"
x:TypeArguments="lib:ViewModel"
BorderThickness="0"
mc:Ignorable="d">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="40" />
<RowDefinition Height="5" />
<RowDefinition Height="25" />
</Grid.RowDefinitions>
<Rectangle Grid.Row="2"
x:Name="OverhangShadowRect"
Height="25"
VerticalAlignment="Top"
IsHitTestVisible="False">
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="#AA000000" />
<GradientStop Offset="1" Color="#00000000" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Grid.Row="1" Fill="{StaticResource BackgroundBrush}" />
<mahapps:MetroProgressBar Grid.Row="1" Grid.RowSpan="2"
x:Name="BottomProgressBarDarkGlow"
Height="16"
Margin="-4"
VerticalAlignment="Top"
Background="Transparent"
BorderBrush="Transparent"
Foreground="{StaticResource PrimaryVariantBrush}"
Maximum="1">
<mahapps:MetroProgressBar.Effect>
<BlurEffect Radius="25" />
</mahapps:MetroProgressBar.Effect>
</mahapps:MetroProgressBar>
<Grid x:Name="TopBarGrid" Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<mahapps:MetroProgressBar Grid.Column="0" Grid.ColumnSpan="4"
x:Name="LargeProgressBar"
Background="#AA121212"
BorderThickness="0"
Maximum="1">
<mahapps:MetroProgressBar.Foreground>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
<GradientStop Offset="0" Color="{StaticResource DarkerPrimaryVariant}" />
<GradientStop Offset="0.5" Color="{StaticResource PrimaryVariant}" />
</LinearGradientBrush>
</mahapps:MetroProgressBar.Foreground>
</mahapps:MetroProgressBar>
<mahapps:MetroProgressBar Grid.Column="0" Grid.ColumnSpan="4"
x:Name="LargeProgressBarTopGlow"
Background="Transparent"
BorderThickness="0"
Maximum="1">
<mahapps:MetroProgressBar.Foreground>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="#33000000" />
<GradientStop Offset="0.3" Color="#00000000" />
</LinearGradientBrush>
</mahapps:MetroProgressBar.Foreground>
</mahapps:MetroProgressBar>
<TextBlock Grid.Column="0"
x:Name="StatePrefixText"
Width="130"
Margin="0,0,0,0"
VerticalAlignment="Center"
FontFamily="Lucida Sans"
FontWeight="Black"
Foreground="{StaticResource ComplementaryBrush}"
TextAlignment="Right" />
<Rectangle Grid.Column="0"
x:Name="PrefixSpacerRect"
Width="50" />
<TextBlock Grid.Column="1"
x:Name="TitleText"
Margin="15,0,0,0"
VerticalAlignment="Center"
FontFamily="Lucida Sans"
FontSize="25"
FontWeight="Black" />
<ContentControl Grid.Column="2" />
</Grid>
<mahapps:MetroProgressBar Grid.Row="1" Grid.RowSpan="2"
x:Name="BottomProgressBarBrightGlow1"
Height="5"
VerticalAlignment="Top"
Background="Transparent"
BorderBrush="Transparent"
Foreground="{StaticResource SecondaryBrush}"
Maximum="1">
<mahapps:MetroProgressBar.Effect>
<BlurEffect Radius="8" />
</mahapps:MetroProgressBar.Effect>
</mahapps:MetroProgressBar>
<mahapps:MetroProgressBar Grid.Row="1" Grid.RowSpan="2"
x:Name="BottomProgressBarBrightGlow2"
Height="5"
VerticalAlignment="Top"
Background="Transparent"
BorderBrush="Transparent"
Foreground="{StaticResource SecondaryBrush}"
Maximum="1">
<mahapps:MetroProgressBar.Effect>
<BlurEffect Radius="15" />
</mahapps:MetroProgressBar.Effect>
</mahapps:MetroProgressBar>
<mahapps:MetroProgressBar Grid.Row="1" Grid.RowSpan="2"
x:Name="BottomProgressBar"
Height="5"
VerticalAlignment="Top"
Background="Transparent"
BorderBrush="Transparent"
Foreground="{StaticResource SecondaryBrush}"
Maximum="1" />
<mahapps:MetroProgressBar Grid.Row="1" Grid.RowSpan="2"
x:Name="BottomProgressBarHighlight"
Height="5"
VerticalAlignment="Top"
Background="Transparent"
BorderBrush="Transparent"
Maximum="1">
<mahapps:MetroProgressBar.Foreground>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="#CCFFFFFF" />
<GradientStop Offset="0.3" Color="#00FFFFFF" />
<GradientStop Offset="0.7" Color="#00FFFFFF" />
<GradientStop Offset="1" Color="#CCFFFFFF" />
</LinearGradientBrush>
</mahapps:MetroProgressBar.Foreground>
</mahapps:MetroProgressBar>
</Grid>
</local:UserControlRx>

View File

@ -0,0 +1,114 @@
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Controls;
using ReactiveUI;
using System;
using ReactiveUI.Fody.Helpers;
using Wabbajack.Lib;
using System.Reactive.Disposables;
namespace Wabbajack
{
/// <summary>
/// Interaction logic for TopProgressView.xaml
/// </summary>
public partial class TopProgressView : UserControlRx<ViewModel>
{
public double ProgressPercent
{
get => (double)GetValue(ProgressPercentProperty);
set => SetValue(ProgressPercentProperty, value);
}
public static readonly DependencyProperty ProgressPercentProperty = DependencyProperty.Register(nameof(ProgressPercent), typeof(double), typeof(TopProgressView),
new FrameworkPropertyMetadata(default(double), WireNotifyPropertyChanged));
public string Title
{
get => (string)GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
public static readonly DependencyProperty TitleProperty = DependencyProperty.Register(nameof(Title), typeof(string), typeof(TopProgressView),
new FrameworkPropertyMetadata(default(string), WireNotifyPropertyChanged));
public string StatePrefixTitle
{
get => (string)GetValue(StatePrefixTitleProperty);
set => SetValue(StatePrefixTitleProperty, value);
}
public static readonly DependencyProperty StatePrefixTitleProperty = DependencyProperty.Register(nameof(StatePrefixTitle), typeof(string), typeof(TopProgressView),
new FrameworkPropertyMetadata(default(string), WireNotifyPropertyChanged));
public bool OverhangShadow
{
get => (bool)GetValue(OverhangShadowProperty);
set => SetValue(OverhangShadowProperty, value);
}
public static readonly DependencyProperty OverhangShadowProperty = DependencyProperty.Register(nameof(OverhangShadow), typeof(bool), typeof(TopProgressView),
new FrameworkPropertyMetadata(true, WireNotifyPropertyChanged));
public bool ShadowMargin
{
get => (bool)GetValue(ShadowMarginProperty);
set => SetValue(ShadowMarginProperty, value);
}
public static readonly DependencyProperty ShadowMarginProperty = DependencyProperty.Register(nameof(ShadowMargin), typeof(bool), typeof(TopProgressView),
new FrameworkPropertyMetadata(true, WireNotifyPropertyChanged));
public TopProgressView()
{
InitializeComponent();
this.WhenActivated(dispose =>
{
this.WhenAny(x => x.ProgressPercent)
.Select(x => 0.3 + x * 0.7)
.BindToStrict(this, x => x.LargeProgressBar.Opacity)
.DisposeWith(dispose);
this.WhenAny(x => x.ProgressPercent)
.BindToStrict(this, x => x.LargeProgressBar.Value)
.DisposeWith(dispose);
this.WhenAny(x => x.ProgressPercent)
.BindToStrict(this, x => x.BottomProgressBarDarkGlow.Value)
.DisposeWith(dispose);
this.WhenAny(x => x.ProgressPercent)
.BindToStrict(this, x => x.LargeProgressBarTopGlow.Value)
.DisposeWith(dispose);
this.WhenAny(x => x.ProgressPercent)
.BindToStrict(this, x => x.BottomProgressBarBrightGlow1.Value)
.DisposeWith(dispose);
this.WhenAny(x => x.ProgressPercent)
.BindToStrict(this, x => x.BottomProgressBarBrightGlow2.Value)
.DisposeWith(dispose);
this.WhenAny(x => x.ProgressPercent)
.BindToStrict(this, x => x.BottomProgressBar.Value)
.DisposeWith(dispose);
this.WhenAny(x => x.ProgressPercent)
.BindToStrict(this, x => x.BottomProgressBarHighlight.Value)
.DisposeWith(dispose);
this.WhenAny(x => x.OverhangShadow)
.Select(x => x ? Visibility.Visible : Visibility.Collapsed)
.BindToStrict(this, x => x.OverhangShadowRect.Visibility)
.DisposeWith(dispose);
this.WhenAny(x => x.ShadowMargin)
.DistinctUntilChanged()
.Select(x => x ? new Thickness(6, 0, 6, 0) : new Thickness(0))
.BindToStrict(this, x => x.OverhangShadowRect.Margin)
.DisposeWith(dispose);
this.WhenAny(x => x.Title)
.BindToStrict(this, x => x.TitleText.Text)
.DisposeWith(dispose);
this.WhenAny(x => x.StatePrefixTitle)
.Select(x => x == null ? Visibility.Visible : Visibility.Collapsed)
.BindToStrict(this, x => x.PrefixSpacerRect.Visibility)
.DisposeWith(dispose);
this.WhenAny(x => x.StatePrefixTitle)
.Select(x => x == null ? Visibility.Collapsed : Visibility.Visible)
.BindToStrict(this, x => x.StatePrefixText.Visibility)
.DisposeWith(dispose);
this.WhenAny(x => x.StatePrefixTitle)
.BindToStrict(this, x => x.StatePrefixText.Text)
.DisposeWith(dispose);
});
}
}
}

View File

@ -0,0 +1,120 @@
<UserControl
x:Class="Wabbajack.UnderMaintenanceOverlay"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:icon="http://metro.mahapps.com/winfx/xaml/iconpacks"
xmlns:local="clr-namespace:Wabbajack"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="250"
d:DesignWidth="800"
mc:Ignorable="d">
<UserControl.Resources>
<Color x:Key="YellowColor">#fec701</Color>
<SolidColorBrush x:Key="YellowBrush" Color="{StaticResource YellowColor}" />
<Color x:Key="TransparentColor">#AA000000</Color>
<SolidColorBrush x:Key="TransparentBrush" Color="{StaticResource TransparentColor}" />
<LinearGradientBrush x:Key="LinesBrush" MappingMode="Absolute" SpreadMethod="Repeat" StartPoint="0,0" EndPoint="48,36">
<GradientStop Offset="0" Color="Black" />
<GradientStop Offset="0.5" Color="{StaticResource TransparentColor}" />
<GradientStop Offset="0.5" Color="{StaticResource YellowColor}" />
<GradientStop Offset="1" Color="{StaticResource YellowColor}" />
</LinearGradientBrush>
</UserControl.Resources>
<Grid Background="{StaticResource TransparentBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="2*" />
</Grid.RowDefinitions>
<Rectangle Grid.Row="0"
Height="65"
Fill="{StaticResource LinesBrush}">
<Rectangle.Effect>
<DropShadowEffect
BlurRadius="15"
Opacity="0.7"
ShadowDepth="10" />
</Rectangle.Effect>
</Rectangle>
<Grid Grid.Row="1"
Margin="30"
Visibility="{Binding ShowHelp, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}, Converter={StaticResource bool2VisibilityConverter}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
Margin="0,0,0,10"
FontFamily="Lucida Sans"
FontSize="30"
FontWeight="Bold"
Foreground="{StaticResource YellowBrush}"
Text="UNDER MAINTENANCE" />
<TextBlock Grid.Row="1"
FontFamily="Lucida Sans"
FontSize="15"
FontWeight="Bold"
Foreground="{StaticResource YellowBrush}"
Text="When mods are updated and the old versions are taken down, Wabbajack can no longer install the modlist for you. &#x0a; &#x0a;Modlist authors need to download the new mod and confirm its correctness before the modlist can be made available again."
TextWrapping="WrapWithOverflow" />
</Grid>
<Viewbox Grid.Row="1"
Margin="20"
VerticalAlignment="Top"
Visibility="{Binding ShowHelp, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}, Converter={StaticResource bool2VisibilityConverter}, ConverterParameter=False}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
FontFamily="Lucida Sans"
FontSize="100"
FontWeight="Bold"
Foreground="{StaticResource YellowBrush}"
Text="UNDER">
<TextBlock.Effect>
<DropShadowEffect
BlurRadius="15"
Opacity="0.7"
ShadowDepth="10" />
</TextBlock.Effect>
</TextBlock>
<Button Grid.Row="0"
Width="45"
Height="45"
Margin="0,10"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Click="Help_Click">
<Button.Style>
<Style BasedOn="{StaticResource IconBareButtonStyle}" TargetType="Button">
<Setter Property="Foreground" Value="{StaticResource YellowBrush}" />
</Style>
</Button.Style>
<icon:PackIconMaterial
Width="35"
Height="35"
Kind="HelpCircle" />
</Button>
<TextBlock Grid.Row="1"
FontFamily="Lucida Sans"
FontSize="100"
FontWeight="Bold"
Foreground="{StaticResource YellowBrush}"
Text="MAINTENANCE">
<TextBlock.Effect>
<DropShadowEffect
BlurRadius="15"
Opacity="0.7"
ShadowDepth="10" />
</TextBlock.Effect>
</TextBlock>
</Grid>
</Viewbox>
</Grid>
</UserControl>

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;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Wabbajack
{
/// <summary>
/// Interaction logic for UnderMaintenanceOverlay.xaml
/// </summary>
public partial class UnderMaintenanceOverlay : UserControl
{
public bool ShowHelp
{
get => (bool)GetValue(ShowHelpProperty);
set => SetValue(ShowHelpProperty, value);
}
public static readonly DependencyProperty ShowHelpProperty = DependencyProperty.Register(nameof(ShowHelp), typeof(bool), typeof(UnderMaintenanceOverlay),
new FrameworkPropertyMetadata(default(bool)));
public UnderMaintenanceOverlay()
{
InitializeComponent();
}
private void Help_Click(object sender, RoutedEventArgs e)
{
ShowHelp = !ShowHelp;
}
}
}

View File

@ -0,0 +1,127 @@
<rxui:ReactiveUserControl
x:Class="Wabbajack.CompilationCompleteView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:icon="http://metro.mahapps.com/winfx/xaml/iconpacks"
xmlns:local="clr-namespace:Wabbajack"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:rxui="http://reactiveui.net"
d:DesignHeight="450"
d:DesignWidth="800"
x:TypeArguments="local:CompilerVM"
mc:Ignorable="d">
<local:AttentionBorder x:Name="AttentionBorder" ClipToBounds="True">
<Grid Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="3*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3"
x:Name="TitleText"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
FontFamily="Lucida Sans"
FontSize="22"
FontWeight="Black">
<TextBlock.Effect>
<DropShadowEffect BlurRadius="25" Opacity="0.5" />
</TextBlock.Effect>
</TextBlock>
<Grid Grid.Row="1" Grid.Column="0"
VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Button Grid.Row="0"
x:Name="BackButton"
Width="55"
Height="55"
Style="{StaticResource CircleButtonStyle}">
<icon:PackIconMaterial
Width="28"
Height="28"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
Kind="ArrowLeft" />
</Button>
<TextBlock Grid.Row="1"
Margin="0,10,0,0"
HorizontalAlignment="Center"
Text="Main Menu" />
</Grid>
<Grid Grid.Row="1" Grid.Column="1"
VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Button
x:Name="GoToModlistButton"
Width="55"
Height="55"
Style="{StaticResource CircleButtonStyle}">
<icon:PackIconMaterial
Width="25"
Height="25"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
Kind="FolderMove" />
</Button>
<TextBlock Grid.Row="1"
x:Name="ActionText"
Margin="0,10,0,0"
HorizontalAlignment="Center" />
</Grid>
<Grid Grid.Row="1" Grid.Column="2"
VerticalAlignment="Center"
Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!--<Button
Width="55"
Height="55"
Background="{StaticResource PrimaryVariantBrush}"
BorderBrush="{StaticResource PrimaryVariantBrush}"
IsHitTestVisible="False"
Style="{StaticResource CircleButtonStyle}">
<Button.Effect>
<BlurEffect Radius="35" />
</Button.Effect>
</Button>
<Button
Width="55"
Height="55"
Background="{StaticResource SecondaryBrush}"
BorderBrush="{StaticResource SecondaryBrush}"
IsHitTestVisible="False"
Style="{StaticResource CircleButtonStyle}">
<Button.Effect>
<BlurEffect Radius="15" />
</Button.Effect>
</Button>-->
<Button
x:Name="CloseWhenCompletedButton"
Width="55"
Height="55"
Style="{StaticResource CircleButtonStyle}">
<icon:PackIconMaterial
Width="30"
Height="30"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
Kind="Check" />
</Button>
<TextBlock Grid.Row="1"
Margin="0,10,0,0"
HorizontalAlignment="Center"
Text="Close" />
</Grid>
</Grid>
</local:AttentionBorder>
</rxui:ReactiveUserControl>

View File

@ -0,0 +1,43 @@
using System.Reactive.Disposables;
using System.Reactive.Linq;
using ReactiveUI;
namespace Wabbajack
{
/// <summary>
/// Interaction logic for CompilationCompleteView.xaml
/// </summary>
public partial class CompilationCompleteView
{
public CompilationCompleteView()
{
InitializeComponent();
this.WhenActivated(dispose =>
{
this.WhenAny(x => x.ViewModel.Completed)
.Select(x => x?.Failed ?? false)
.BindToStrict(this, x => x.AttentionBorder.Failure)
.DisposeWith(dispose);
this.WhenAny(x => x.ViewModel.Completed)
.Select(x => x?.Failed ?? false)
.Select(failed => $"Compilation {(failed ? "Failed" : "Complete")}")
.BindToStrict(this, x => x.TitleText.Text)
.DisposeWith(dispose);
this.WhenAny(x => x.ViewModel.Completed)
.Select(x => x?.Failed ?? false)
.Select(failed => failed ? "Open Logs Folder" : "Go to Modlist")
.BindToStrict(this, x => x.ActionText.Text)
.DisposeWith(dispose);
this.WhenAny(x => x.ViewModel.BackCommand)
.BindToStrict(this, x => x.BackButton.Command)
.DisposeWith(dispose);
this.WhenAny(x => x.ViewModel.GoToCommand)
.BindToStrict(this, x => x.GoToModlistButton.Command)
.DisposeWith(dispose);
this.WhenAny(x => x.ViewModel.CloseWhenCompleteCommand)
.BindToStrict(this, x => x.CloseWhenCompletedButton.Command)
.DisposeWith(dispose);
});
}
}
}

View File

@ -0,0 +1,220 @@
<rxui:ReactiveUserControl
x:Class="Wabbajack.CompilerView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:lib="clr-namespace:Wabbajack.Lib;assembly=Wabbajack.Lib"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:icon="http://metro.mahapps.com/winfx/xaml/iconpacks"
xmlns:local="clr-namespace:Wabbajack"
xmlns:mahapps="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:rxui="http://reactiveui.net"
xmlns:wabbacommon="clr-namespace:Wabbajack.Common;assembly=Wabbajack.Common"
d:DataContext="{d:DesignInstance local:CompilerVM}"
d:DesignHeight="450"
d:DesignWidth="800"
x:TypeArguments="local:CompilerVM"
mc:Ignorable="d">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="47" />
<RowDefinition Height="4*" />
<RowDefinition Height="*" MinHeight="150" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="5" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="5*" />
<ColumnDefinition Width="5" />
</Grid.ColumnDefinitions>
<local:HeatedBackgroundView Grid.Row="0" Grid.RowSpan="3" Grid.Column="0" Grid.ColumnSpan="5"
x:Name="HeatedBackground" />
<Border Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2"
x:Name="ModlistDetailsBackground"
Margin="1,1,5,0"
Background="{StaticResource WindowBackgroundBrush}"
BorderThickness="0,0,1,1">
<Border.BorderBrush>
<LinearGradientBrush StartPoint="1,0" EndPoint="0,0">
<GradientStop Offset="0.5" Color="{StaticResource DarkBackgroundColor}" />
<GradientStop Offset="1" Color="{StaticResource WindowBackgroundColor}" />
</LinearGradientBrush>
</Border.BorderBrush>
</Border>
<Border Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2"
x:Name="ModlistDetailsHeatBorder"
Margin="1,1,5,0"
BorderBrush="{StaticResource HeatedBorderBrush}"
BorderThickness="1" />
<Border Grid.Row="1" Grid.Column="3"
BorderBrush="{StaticResource BorderInterestBrush}"
BorderThickness="1,0,1,1">
<local:DetailImageView x:Name="DetailImage" BorderThickness="0" />
</Border>
<!-- Comes after image area so shadow can overlay -->
<local:TopProgressView Grid.Row="0" Grid.RowSpan="2" Grid.Column="0" Grid.ColumnSpan="5"
x:Name="TopProgressBar"
OverhangShadow="True" />
<Button Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="5"
x:Name="BackButton"
Width="30"
Height="30"
Margin="12,5,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Style="{StaticResource IconCircleButtonStyle}"
ToolTip="Back to main menu">
<icon:PackIconMaterial Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}" Kind="ArrowLeft" />
</Button>
<ScrollViewer Grid.Row="1" Grid.Column="1"
x:Name="SettingsScrollViewer"
Margin="5"
Background="Transparent"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<StackPanel
Margin="0,5,0,0"
Background="Transparent"
Orientation="Vertical">
<StackPanel.Resources>
<Thickness
x:Key="TitleMargin"
Bottom="1"
Left="5" />
<Style
x:Key="ValueStyle"
BasedOn="{StaticResource MainTextBoxStyle}"
TargetType="TextBox">
<Setter Property="MaxLength" Value="150" />
<Setter Property="AcceptsTab" Value="False" />
<Setter Property="FontSize" Value="15" />
<Setter Property="Margin" Value="0,0,0,6" />
</Style>
<Style
x:Key="PickerStyle"
BasedOn="{StaticResource MainFilePickerStyle}"
TargetType="local:FilePicker">
<Setter Property="Margin" Value="0,0,0,6" />
</Style>
</StackPanel.Resources>
<TextBlock Margin="{StaticResource TitleMargin}" Text="ModList Name" />
<TextBox x:Name="ModListNameSetting" Style="{StaticResource ValueStyle}" />
<TextBlock Margin="{StaticResource TitleMargin}" Text="Version" />
<TextBox
x:Name="VersionSetting"
MaxLength="9"
Style="{StaticResource ValueStyle}" />
<TextBlock Margin="{StaticResource TitleMargin}" Text="Author" />
<TextBox x:Name="AuthorNameSetting" Style="{StaticResource ValueStyle}" />
<TextBlock Margin="{StaticResource TitleMargin}" Text="Description" />
<TextBox
x:Name="DescriptionSetting"
Height="150"
mahapps:TextBoxHelper.Watermark="(700 characters max)"
AcceptsReturn="True"
AcceptsTab="False"
MaxLength="700"
Style="{StaticResource ValueStyle}"
TextWrapping="Wrap" />
<TextBlock Margin="{StaticResource TitleMargin}" Text="Image" />
<local:FilePicker
x:Name="ImageFilePicker"
Style="{StaticResource PickerStyle}"
ToolTip="Path to an image to display for the modlist." />
<TextBlock Margin="{StaticResource TitleMargin}" Text="Website" />
<TextBox x:Name="WebsiteSetting" Style="{StaticResource ValueStyle}" />
<TextBlock
Margin="{StaticResource TitleMargin}"
Text="Readme"
ToolTip="Link to the Readme." />
<TextBox x:Name="ReadmeSetting" Style="{StaticResource ValueStyle}" />
<CheckBox
x:Name="NSFWSetting"
Content="NSFW"
ToolTip="Select this if your Modlist has adult themed content such as SexLab or other mods involving sexual acts. Nude body replacer do not fall under this category neither do revealing outfits or gore." />
<CheckBox
x:Name="PublishUpdate"
Content="Publish Update"
ToolTip="Select this if your want Wabbajack to automatically publish this modlist when compilation finished (requires a selected machineURL)" />
<TextBlock
Margin="{StaticResource TitleMargin}"
Text="MachineUrl"
ToolTip="If this box has a value the modlist will be published to this MachineUrl after compilation" />
<TextBox x:Name="MachineUrl" Style="{StaticResource ValueStyle}" />
</StackPanel>
</ScrollViewer>
<Border Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="5"
x:Name="BottomBarBackground"
Margin="5"
Background="{StaticResource WindowBackgroundBrush}" />
<Grid Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="5"
MaxWidth="1000">
<Grid
x:Name="BottomCompilerSettingsGrid"
Margin="35,0,35,0"
VerticalAlignment="Center"
ClipToBounds="False">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="20" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid Grid.Row="0" Grid.RowSpan="5" Grid.Column="0"
Margin="15"
VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<local:ImageRadioButtonView Grid.Row="0"
x:Name="MO2CompilerButton"
Height="35"
Margin="4"
IsChecked="{Binding SelectedCompilerType, Converter={StaticResource EqualsToBoolConverter}, ConverterParameter={x:Static wabbacommon:ModManager.MO2}}">
<local:ImageRadioButtonView.Image>
<BitmapImage UriSource="../../Resources/MO2Button.png" />
</local:ImageRadioButtonView.Image>
</local:ImageRadioButtonView>
</Grid>
<ContentPresenter Grid.Row="1" Grid.Column="1"
x:Name="CustomCompilerSettingsPresenter">
<ContentPresenter.Resources>
<DataTemplate DataType="{x:Type local:MO2CompilerVM}">
<local:MO2CompilerConfigView />
</DataTemplate>
</ContentPresenter.Resources>
</ContentPresenter>
<local:BeginButton Grid.Row="0" Grid.RowSpan="3" Grid.Column="5"
x:Name="BeginButton" />
</Grid>
</Grid>
<Grid Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="5"
x:Name="MidCompilationGrid"
Margin="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="4*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="3*" />
</Grid.ColumnDefinitions>
<local:LogView x:Name="LogView" Grid.Column="0" />
<local:CpuView Grid.Column="2"
x:Name="CpuView"
ViewModel="{Binding}" />
<local:AttentionBorder x:Name="UserInterventionsControl" Grid.Column="2">
<Grid>
<local:ConfirmationInterventionView DataContext="{Binding ActiveGlobalUserIntervention}" Visibility="{Binding ActiveGlobalUserIntervention, Converter={StaticResource IsTypeVisibilityConverter}, ConverterParameter={x:Type lib:ConfirmationIntervention}}" />
</Grid>
</local:AttentionBorder>
<local:CompilationCompleteView Grid.Column="2"
x:Name="CompilationComplete"
ViewModel="{Binding}" />
</Grid>
</Grid>
</rxui:ReactiveUserControl>

View File

@ -0,0 +1,132 @@
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Windows.Controls;
using ReactiveUI;
using System.Windows;
using DynamicData;
using Wabbajack.Common;
namespace Wabbajack
{
/// <summary>
/// Interaction logic for CompilerView.xaml
/// </summary>
public partial class CompilerView : ReactiveUserControl<CompilerVM>
{
public CompilerView()
{
InitializeComponent();
this.WhenActivated(dispose =>
{
// Bind percent completed chanes
this.WhenAny(x => x.ViewModel.PercentCompleted)
.Select(f => (double)f)
.BindToStrict(this, x => x.HeatedBackground.PercentCompleted)
.DisposeWith(dispose);
this.WhenAny(x => x.ViewModel.PercentCompleted)
.Select(f => (double)f)
.BindToStrict(this, x => x.ModlistDetailsHeatBorder.Opacity)
.DisposeWith(dispose);
this.WhenAny(x => x.ViewModel.PercentCompleted)
.Select(f => (double)f)
.BindToStrict(this, x => x.TopProgressBar.ProgressPercent)
.DisposeWith(dispose);
// Bind detail image display
this.WhenAny(x => x.ViewModel.CurrentModlistSettings.ModListName)
.BindToStrict(this, x => x.DetailImage.Title)
.DisposeWith(dispose);
this.WhenAny(x => x.ViewModel.CurrentModlistSettings.AuthorText)
.BindToStrict(this, x => x.DetailImage.Author)
.DisposeWith(dispose);
this.WhenAny(x => x.ViewModel.CurrentModlistSettings.Description)
.BindToStrict(this, x => x.DetailImage.Description)
.DisposeWith(dispose);
this.WhenAny(x => x.ViewModel.Image)
.BindToStrict(this, x => x.DetailImage.Image)
.DisposeWith(dispose);
// Top Progress Bar
this.WhenAny(x => x.ViewModel.CurrentModlistSettings.ModListName)
.BindToStrict(this, x => x.TopProgressBar.Title)
.DisposeWith(dispose);
this.WhenAny(x => x.ViewModel.ProgressTitle)
.BindToStrict(this, x => x.TopProgressBar.StatePrefixTitle)
.DisposeWith(dispose);
this.WhenAny(x => x.ViewModel.BackCommand)
.BindToStrict(this, x => x.BackButton.Command)
.DisposeWith(dispose);
// Settings Panel
this.WhenAny(x => x.ViewModel.Compiling)
.Select(x => !x)
.BindToStrict(this, x => x.SettingsScrollViewer.IsEnabled)
.DisposeWith(dispose);
this.BindStrict(ViewModel, x => x.CurrentModlistSettings.ModListName, x => x.ModListNameSetting.Text)
.DisposeWith(dispose);
this.BindStrict(ViewModel, x => x.CurrentModlistSettings.VersionText, x => x.VersionSetting.Text)
.DisposeWith(dispose);
this.BindStrict(ViewModel, x => x.CurrentModlistSettings.AuthorText, x => x.AuthorNameSetting.Text)
.DisposeWith(dispose);
this.BindStrict(ViewModel, x => x.CurrentModlistSettings.Description, x => x.DescriptionSetting.Text)
.DisposeWith(dispose);
this.WhenAny(x => x.ViewModel.CurrentModlistSettings.ImagePath)
.BindToStrict(this, x => x.ImageFilePicker.PickerVM)
.DisposeWith(dispose);
this.BindStrict(ViewModel, x => x.CurrentModlistSettings.Website, x => x.WebsiteSetting.Text)
.DisposeWith(dispose);
this.BindStrict(ViewModel, x => x.CurrentModlistSettings.Readme, x => x.ReadmeSetting.Text)
.DisposeWith(dispose);
this.BindStrict(ViewModel, x => x.CurrentModlistSettings.IsNSFW, x => x.NSFWSetting.IsChecked)
.DisposeWith(dispose);
this.BindStrict(ViewModel, x => x.CurrentModlistSettings.MachineUrl, x => x.MachineUrl.Text)
.DisposeWith(dispose);
this.BindStrict(ViewModel, x => x.CurrentModlistSettings.Publish, x => x.PublishUpdate.IsChecked)
.DisposeWith(dispose);
// Bottom Compiler Settings
this.WhenAny(x => x.ViewModel.StartedCompilation)
.Select(started => started ? Visibility.Hidden : Visibility.Visible)
.BindToStrict(this, x => x.BottomCompilerSettingsGrid.Visibility)
.DisposeWith(dispose);
this.WhenAny(x => x.ViewModel.Compiler)
.BindToStrict(this, x => x.CustomCompilerSettingsPresenter.Content)
.DisposeWith(dispose);
this.WhenAny(x => x.ViewModel.BeginCommand)
.BindToStrict(this, x => x.BeginButton.Command)
.DisposeWith(dispose);
// Mid-compilation panel
this.WhenAny(x => x.ViewModel.StartedCompilation)
.Select(started => started ? Visibility.Visible : Visibility.Hidden)
.BindToStrict(this, x => x.MidCompilationGrid.Visibility)
.DisposeWith(dispose);
this.WhenAny(x => x.ViewModel.PercentCompleted)
.Select(f => (double)f)
.BindToStrict(this, x => x.LogView.ProgressPercent)
.DisposeWith(dispose);
this.WhenAny(x => x.ViewModel.PercentCompleted)
.BindToStrict(this, x => x.CpuView.ProgressPercent)
.DisposeWith(dispose);
this.WhenAny(x => x.ViewModel.MWVM.Settings)
.BindToStrict(this, x => x.CpuView.SettingsHook)
.DisposeWith(dispose);
this.WhenAny(x => x.ViewModel.ActiveGlobalUserIntervention)
.Select(x => x == null ? Visibility.Visible : Visibility.Collapsed)
.BindToStrict(this, x => x.CpuView.Visibility)
.DisposeWith(dispose);
this.WhenAny(x => x.ViewModel.ActiveGlobalUserIntervention)
.Select(x => x != null ? Visibility.Visible : Visibility.Collapsed)
.BindToStrict(this, x => x.UserInterventionsControl.Visibility)
.DisposeWith(dispose);
this.WhenAny(x => x.ViewModel.Completed)
.Select(x => x != null ? Visibility.Visible : Visibility.Collapsed)
.BindToStrict(this, x => x.CompilationComplete.Visibility)
.DisposeWith(dispose);
});
}
}
}

View File

@ -0,0 +1,63 @@
<UserControl
x:Class="Wabbajack.MO2CompilerConfigView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Wabbajack"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DataContext="{d:DesignInstance local:MO2CompilerVM}"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="20" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="40" />
<RowDefinition Height="40" />
<RowDefinition Height="40" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
FontSize="14"
Text="Modlist Location"
TextAlignment="Center"
ToolTip="The MO2 modlist.txt file you want to use as your source" />
<local:FilePicker Grid.Row="0" Grid.Column="2"
Height="30"
VerticalAlignment="Center"
FontSize="14"
PickerVM="{Binding ModListLocation}"
ToolTip="The MO2 modlist.txt file you want to use as your source" />
<TextBlock Grid.Row="1" Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
FontSize="14"
Text="Download Location"
TextAlignment="Center"
ToolTip="The folder where MO2 downloads your mods." />
<local:FilePicker Grid.Row="1" Grid.Column="2"
Height="30"
VerticalAlignment="Center"
FontSize="14"
PickerVM="{Binding DownloadLocation}"
ToolTip="The folder where MO2 downloads your mods." />
<TextBlock Grid.Row="2" Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
FontSize="14"
Text="Output Location"
TextAlignment="Center"
ToolTip="The folder to place the resulting modlist.wabbajack file" />
<local:FilePicker Grid.Row="2" Grid.Column="2"
Height="30"
VerticalAlignment="Center"
FontSize="14"
PickerVM="{Binding Parent.OutputLocation}"
ToolTip="The folder to place the resulting modlist.wabbajack file" />
</Grid>
</UserControl>

View File

@ -0,0 +1,15 @@
using System.Windows.Controls;
namespace Wabbajack
{
/// <summary>
/// Interaction logic for MO2CompilerConfigView.xaml
/// </summary>
public partial class MO2CompilerConfigView : UserControl
{
public MO2CompilerConfigView()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,154 @@
<rxui:ReactiveUserControl
x:Class="Wabbajack.InstallationCompleteView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:icon="http://metro.mahapps.com/winfx/xaml/iconpacks"
xmlns:local="clr-namespace:Wabbajack"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:rxui="http://reactiveui.net"
d:DesignHeight="450"
d:DesignWidth="800"
x:TypeArguments="local:InstallerVM"
mc:Ignorable="d">
<local:AttentionBorder x:Name="AttentionBorder" ClipToBounds="True">
<Grid Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="3*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="4"
x:Name="TitleText"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
FontFamily="Lucida Sans"
FontSize="22"
FontWeight="Black">
<TextBlock.Effect>
<DropShadowEffect BlurRadius="25" Opacity="0.5" />
</TextBlock.Effect>
</TextBlock>
<Grid Grid.Row="1" Grid.Column="0"
VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Button Grid.Row="0"
x:Name="BackButton"
Width="50"
Height="50"
Style="{StaticResource CircleButtonStyle}">
<icon:PackIconMaterial
Width="25"
Height="25"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
Kind="ArrowLeft" />
</Button>
<TextBlock Grid.Row="1"
Margin="0,10,0,0"
HorizontalAlignment="Center"
Text="Main Menu" />
</Grid>
<Grid Grid.Row="1" Grid.Column="1"
VerticalAlignment="Center"
Visibility="{Binding InstallerSupportsAfterInstallNavigation, Converter={StaticResource bool2VisibilityConverter}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Button
x:Name="GoToInstallButton"
Width="50"
Height="50"
Style="{StaticResource CircleButtonStyle}">
<icon:PackIconMaterial
Width="23"
Height="23"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
Kind="FolderMove" />
</Button>
<TextBlock Grid.Row="1"
Margin="0,10,0,0"
HorizontalAlignment="Center"
Text="Install Folder" />
</Grid>
<Grid Grid.Row="1" Grid.Column="2"
VerticalAlignment="Center"
Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Button
x:Name="OpenLogsButton"
Width="50"
Height="50"
Style="{StaticResource CircleButtonStyle}">
<icon:PackIconFontAwesome
Width="25"
Height="25"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
Kind="ListAltRegular" />
</Button>
<TextBlock Grid.Row="1"
Margin="0,10,0,0"
HorizontalAlignment="Center"
Text="Log Files" />
</Grid>
<Grid Grid.Row="1" Grid.Column="3"
VerticalAlignment="Center"
Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Button
x:Name="OpenReadmeButton"
Width="50"
Height="50"
Style="{StaticResource CircleButtonStyle}">
<icon:PackIconFontAwesome
Width="25"
Height="25"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
Kind="ReadmeBrands" />
</Button>
<TextBlock Grid.Row="1"
Margin="0,10,0,0"
HorizontalAlignment="Center"
Text="Readme" />
</Grid>
<Grid Grid.Row="1" Grid.Column="4"
VerticalAlignment="Center"
Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Button
x:Name="CloseButton"
Width="50"
Height="50"
Style="{StaticResource CircleButtonStyle}">
<icon:PackIconMaterial
Width="25"
Height="25"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
Kind="Check" />
</Button>
<TextBlock Grid.Row="1"
Margin="0,10,0,0"
HorizontalAlignment="Center"
Text="Close" />
</Grid>
</Grid>
</local:AttentionBorder>
</rxui:ReactiveUserControl>

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