Initial import of WPF code
18
Wabbajack.App.Wpf/App.xaml
Normal 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>
|
26
Wabbajack.App.Wpf/App.xaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
57
Wabbajack.App.Wpf/ClientAPIAdditions/ClientAPIEx.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
32
Wabbajack.App.Wpf/Converters/BoolToVisibilityConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
35
Wabbajack.App.Wpf/Converters/CommandConverter.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
32
Wabbajack.App.Wpf/Converters/ConverterRegistration.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
19
Wabbajack.App.Wpf/Converters/EqualsToBoolConverter.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
20
Wabbajack.App.Wpf/Converters/FileSizeConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
35
Wabbajack.App.Wpf/Converters/IntDownCastConverter.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
24
Wabbajack.App.Wpf/Converters/InverseBooleanConverter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
37
Wabbajack.App.Wpf/Converters/IsNotNullVisibilityConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
32
Wabbajack.App.Wpf/Converters/IsTypeVisibilityConverter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
80
Wabbajack.App.Wpf/Converters/PercentToDoubleConverter.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
82
Wabbajack.App.Wpf/Extensions/DynamicDataExt.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
118
Wabbajack.App.Wpf/Extensions/IViewForExt.cs
Normal 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
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
3
Wabbajack.App.Wpf/FodyWeavers.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<ReactiveUI />
|
||||
</Weavers>
|
26
Wabbajack.App.Wpf/FodyWeavers.xsd
Normal 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>
|
27
Wabbajack.App.Wpf/Properties/AssemblyInfo.cs
Normal 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)
|
||||
)]
|
63
Wabbajack.App.Wpf/Properties/Resources.Designer.cs
generated
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
117
Wabbajack.App.Wpf/Properties/Resources.resx
Normal 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>
|
26
Wabbajack.App.Wpf/Properties/Settings.Designer.cs
generated
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
8
Wabbajack.App.Wpf/Properties/Settings.settings
Normal 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>
|
4
Wabbajack.App.Wpf/Readme.md
Normal 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.
|
BIN
Wabbajack.App.Wpf/Resources/Icons/gog.png
Normal file
After Width: | Height: | Size: 118 KiB |
BIN
Wabbajack.App.Wpf/Resources/Icons/steam.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
Wabbajack.App.Wpf/Resources/Icons/wabbajack.ico
Normal file
After Width: | Height: | Size: 139 KiB |
BIN
Wabbajack.App.Wpf/Resources/MO2Button.png
Normal file
After Width: | Height: | Size: 59 KiB |
20
Wabbajack.App.Wpf/Resources/ResourceLinks.cs
Normal 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));
|
||||
}
|
||||
}
|
BIN
Wabbajack.App.Wpf/Resources/VortexButton.png
Normal file
After Width: | Height: | Size: 44 KiB |
BIN
Wabbajack.App.Wpf/Resources/Wabba_Ded.png
Normal file
After Width: | Height: | Size: 182 KiB |
BIN
Wabbajack.App.Wpf/Resources/Wabba_Mouth.png
Normal file
After Width: | Height: | Size: 272 KiB |
BIN
Wabbajack.App.Wpf/Resources/Wabba_Mouth_No_Text.png
Normal file
After Width: | Height: | Size: 174 KiB |
BIN
Wabbajack.App.Wpf/Resources/Wabba_Mouth_Small.png
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
Wabbajack.App.Wpf/Resources/middle_mouse_button.png
Normal file
After Width: | Height: | Size: 18 KiB |
197
Wabbajack.App.Wpf/Settings.cs
Normal 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>();
|
||||
}
|
||||
|
||||
}
|
118
Wabbajack.App.Wpf/Themes/CustomControls.xaml
Normal 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>
|
3535
Wabbajack.App.Wpf/Themes/Styles.xaml
Normal file
21
Wabbajack.App.Wpf/Util/AsyncLazy.cs
Normal 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())
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
115
Wabbajack.App.Wpf/Util/AutoScrollBehavior.cs
Normal 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) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
278
Wabbajack.App.Wpf/Util/FilePickerVM.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
117
Wabbajack.App.Wpf/Util/SystemParametersConstructor.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
28
Wabbajack.App.Wpf/Util/TreeViewItemExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
166
Wabbajack.App.Wpf/Util/UIUtils.cs
Normal 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]);
|
||||
}
|
||||
}
|
||||
}
|
74
Wabbajack.App.Wpf/View Models/BackNavigatingVM.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
48
Wabbajack.App.Wpf/View Models/CPUDisplayVM.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
272
Wabbajack.App.Wpf/View Models/Compilers/CompilerVM.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
17
Wabbajack.App.Wpf/View Models/Compilers/ISubCompilerVM.cs
Normal 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();
|
||||
}
|
||||
}
|
268
Wabbajack.App.Wpf/View Models/Compilers/MO2CompilerVM.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
209
Wabbajack.App.Wpf/View Models/Gallery/ModListGalleryVM.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
261
Wabbajack.App.Wpf/View Models/Gallery/ModListMetadataVM.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
16
Wabbajack.App.Wpf/View Models/GameVM.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
24
Wabbajack.App.Wpf/View Models/Installers/ISubInstallerVM.cs
Normal 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);
|
||||
}
|
||||
}
|
461
Wabbajack.App.Wpf/View Models/Installers/InstallerVM.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
202
Wabbajack.App.Wpf/View Models/Installers/MO2InstallerVM.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
17
Wabbajack.App.Wpf/View Models/Interfaces/ICpuStatusVM.cs
Normal 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; }
|
||||
}
|
||||
}
|
191
Wabbajack.App.Wpf/View Models/MainWindowVM.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
78
Wabbajack.App.Wpf/View Models/ModListContentsVM.cs
Normal 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; }
|
||||
}
|
||||
}
|
117
Wabbajack.App.Wpf/View Models/ModListVM.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
31
Wabbajack.App.Wpf/View Models/ModVM.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
41
Wabbajack.App.Wpf/View Models/ModeSelectionVM.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
83
Wabbajack.App.Wpf/View Models/Settings/AuthorFilesVM.cs
Normal 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));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
99
Wabbajack.App.Wpf/View Models/Settings/CredentialsLoginVM.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
147
Wabbajack.App.Wpf/View Models/Settings/LoginManagerVM.cs
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
47
Wabbajack.App.Wpf/View Models/Settings/SettingsVM.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
161
Wabbajack.App.Wpf/View Models/SlideShow.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
211
Wabbajack.App.Wpf/View Models/UserInterventionHandlers.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
54
Wabbajack.App.Wpf/View Models/WebBrowserVM.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
31
Wabbajack.App.Wpf/Views/Common/AttentionBorder.cs
Normal 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)));
|
||||
}
|
||||
}
|
85
Wabbajack.App.Wpf/Views/Common/BeginButton.xaml
Normal 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>
|
25
Wabbajack.App.Wpf/Views/Common/BeginButton.xaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
35
Wabbajack.App.Wpf/Views/Common/CpuLineView.xaml
Normal 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>
|
51
Wabbajack.App.Wpf/Views/Common/CpuLineView.xaml.cs
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
32
Wabbajack.App.Wpf/Views/Common/CpuView.xaml
Normal 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>
|
67
Wabbajack.App.Wpf/Views/Common/CpuView.xaml.cs
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
163
Wabbajack.App.Wpf/Views/Common/DetailImageView.xaml
Normal 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>
|
131
Wabbajack.App.Wpf/Views/Common/DetailImageView.xaml.cs
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
156
Wabbajack.App.Wpf/Views/Common/FilePicker.xaml
Normal 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>
|
27
Wabbajack.App.Wpf/Views/Common/FilePicker.xaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
29
Wabbajack.App.Wpf/Views/Common/HeatedBackgroundView.xaml
Normal 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>
|
36
Wabbajack.App.Wpf/Views/Common/HeatedBackgroundView.xaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
26
Wabbajack.App.Wpf/Views/Common/LogView.xaml
Normal 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>
|
36
Wabbajack.App.Wpf/Views/Common/LogView.xaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
71
Wabbajack.App.Wpf/Views/Common/RadioButtonView.xaml
Normal 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>
|
47
Wabbajack.App.Wpf/Views/Common/RadioButtonView.xaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
147
Wabbajack.App.Wpf/Views/Common/TopProgressView.xaml
Normal 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>
|
114
Wabbajack.App.Wpf/Views/Common/TopProgressView.xaml.cs
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
120
Wabbajack.App.Wpf/Views/Common/UnderMaintenanceOverlay.xaml
Normal 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. 
 
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>
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
127
Wabbajack.App.Wpf/Views/Compilers/CompilationCompleteView.xaml
Normal 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>
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
220
Wabbajack.App.Wpf/Views/Compilers/CompilerView.xaml
Normal 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>
|
132
Wabbajack.App.Wpf/Views/Compilers/CompilerView.xaml.cs
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
63
Wabbajack.App.Wpf/Views/Compilers/MO2CompilerConfigView.xaml
Normal 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>
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
154
Wabbajack.App.Wpf/Views/Installers/InstallationCompleteView.xaml
Normal 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>
|