mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
25
Wabbajack.Common/Extensions/DictionaryExt.cs
Normal file
25
Wabbajack.Common/Extensions/DictionaryExt.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
public static class DictionaryExt
|
||||
{
|
||||
public static V TryCreate<K, V>(this IDictionary<K, V> dict, K key)
|
||||
where V : new()
|
||||
{
|
||||
return dict.TryCreate(key, () => new V());
|
||||
}
|
||||
|
||||
public static V TryCreate<K, V>(this IDictionary<K, V> dict, K key, Func<V> create)
|
||||
{
|
||||
if (dict.TryGetValue(key, out var val)) return val;
|
||||
var ret = create();
|
||||
dict[key] = ret;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
172
Wabbajack.Common/Extensions/RxExt.cs
Normal file
172
Wabbajack.Common/Extensions/RxExt.cs
Normal file
@ -0,0 +1,172 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Concurrency;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
public static class RxExt
|
||||
{
|
||||
/// <summary>
|
||||
/// Convenience function that discards events that are null
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="source"></param>
|
||||
/// <returns>Source events that are not null</returns>
|
||||
public static IObservable<T> NotNull<T>(this IObservable<T> source)
|
||||
where T : class
|
||||
{
|
||||
return source.Where(u => u != null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts any observable to type Unit. Useful for when you care that a signal occurred,
|
||||
/// but don't care about what its value is downstream.
|
||||
/// </summary>
|
||||
/// <returns>An observable that returns Unit anytime the source signal fires an event.</returns>
|
||||
public static IObservable<Unit> Unit<T>(this IObservable<T> source)
|
||||
{
|
||||
return source.Select(_ => System.Reactive.Unit.Default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convenience operator to subscribe to the source observable, only when a second "switch" observable is on.
|
||||
/// When the switch is on, the source will be subscribed to, and its updates passed through.
|
||||
/// When the switch is off, the subscription to the source observable will be stopped, and no signal will be published.
|
||||
/// </summary>
|
||||
/// <param name="source">Source observable to subscribe to if on</param>
|
||||
/// <param name="filterSwitch">On/Off signal of whether to subscribe to source observable</param>
|
||||
/// <returns>Observable that publishes data from source, if the switch is on.</returns>
|
||||
public static IObservable<T> FilterSwitch<T>(this IObservable<T> source, IObservable<bool> filterSwitch)
|
||||
{
|
||||
return filterSwitch
|
||||
.DistinctUntilChanged()
|
||||
.Select(on =>
|
||||
{
|
||||
if (on)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Observable.Empty<T>();
|
||||
}
|
||||
})
|
||||
.Switch();
|
||||
}
|
||||
|
||||
|
||||
/// Inspiration:
|
||||
/// http://reactivex.io/documentation/operators/debounce.html
|
||||
/// https://stackoverflow.com/questions/20034476/how-can-i-use-reactive-extensions-to-throttle-events-using-a-max-window-size
|
||||
public static IObservable<T> Debounce<T>(this IObservable<T> source, TimeSpan interval, IScheduler scheduler = null)
|
||||
{
|
||||
scheduler = scheduler ?? Scheduler.Default;
|
||||
return Observable.Create<T>(o =>
|
||||
{
|
||||
var hasValue = false;
|
||||
bool throttling = false;
|
||||
T value = default;
|
||||
|
||||
var dueTimeDisposable = new SerialDisposable();
|
||||
|
||||
void internalCallback()
|
||||
{
|
||||
if (hasValue)
|
||||
{
|
||||
// We have another value that came in to fire.
|
||||
// Reregister for callback
|
||||
dueTimeDisposable.Disposable = scheduler.Schedule(interval, internalCallback);
|
||||
o.OnNext(value);
|
||||
value = default;
|
||||
hasValue = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Nothing to do, throttle is complete.
|
||||
throttling = false;
|
||||
}
|
||||
}
|
||||
|
||||
return source.Subscribe(
|
||||
onNext: (x) =>
|
||||
{
|
||||
if (!throttling)
|
||||
{
|
||||
// Fire initial value
|
||||
o.OnNext(x);
|
||||
// Mark that we're throttling
|
||||
throttling = true;
|
||||
// Register for callback when throttle is complete
|
||||
dueTimeDisposable.Disposable = scheduler.Schedule(interval, internalCallback);
|
||||
}
|
||||
else
|
||||
{
|
||||
// In the middle of throttle
|
||||
// Save value and return
|
||||
hasValue = true;
|
||||
value = x;
|
||||
}
|
||||
},
|
||||
onError: o.OnError,
|
||||
onCompleted: o.OnCompleted);
|
||||
});
|
||||
}
|
||||
|
||||
public static IObservable<Unit> SelectTask<T>(this IObservable<T> source, Func<T, Task> task)
|
||||
{
|
||||
return source
|
||||
.SelectMany(async i =>
|
||||
{
|
||||
await task(i).ConfigureAwait(false);
|
||||
return System.Reactive.Unit.Default;
|
||||
});
|
||||
}
|
||||
|
||||
public static IObservable<Unit> SelectTask<T>(this IObservable<T> source, Func<Task> task)
|
||||
{
|
||||
return source
|
||||
.SelectMany(async _ =>
|
||||
{
|
||||
await task().ConfigureAwait(false);
|
||||
return System.Reactive.Unit.Default;
|
||||
});
|
||||
}
|
||||
|
||||
public static IObservable<R> SelectTask<T, R>(this IObservable<T> source, Func<Task<R>> task)
|
||||
{
|
||||
return source
|
||||
.SelectMany(_ => task());
|
||||
}
|
||||
|
||||
public static IObservable<R> SelectTask<T, R>(this IObservable<T> source, Func<T, Task<R>> task)
|
||||
{
|
||||
return source
|
||||
.SelectMany(x => task(x));
|
||||
}
|
||||
|
||||
public static IObservable<T> DoTask<T>(this IObservable<T> source, Func<T, Task> task)
|
||||
{
|
||||
return source
|
||||
.SelectMany(async (x) =>
|
||||
{
|
||||
await task(x).ConfigureAwait(false);
|
||||
return x;
|
||||
});
|
||||
}
|
||||
|
||||
public static IObservable<R> WhereCastable<T, R>(this IObservable<T> source)
|
||||
where R : class
|
||||
where T : class
|
||||
{
|
||||
return source
|
||||
.Select(x => x as R)
|
||||
.NotNull();
|
||||
}
|
||||
}
|
||||
}
|
@ -92,7 +92,10 @@
|
||||
<Compile Include="Error States\ErrorResponse.cs" />
|
||||
<Compile Include="Error States\GetResponse.cs" />
|
||||
<Compile Include="ExtensionManager.cs" />
|
||||
<Compile Include="Extensions\DictionaryExt.cs" />
|
||||
<Compile Include="Extensions\HashHelper.cs" />
|
||||
<Compile Include="Extensions\RxExt.cs" />
|
||||
<Compile Include="Extensions\TaskExt.cs" />
|
||||
<Compile Include="FileExtractor.cs" />
|
||||
<Compile Include="GameMetaData.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
|
@ -92,10 +92,11 @@ namespace Wabbajack.Lib
|
||||
}
|
||||
}
|
||||
|
||||
public static string OpenFileDialog(string filter)
|
||||
public static string OpenFileDialog(string filter, string initialDirectory = null)
|
||||
{
|
||||
OpenFileDialog ofd = new OpenFileDialog();
|
||||
ofd.Filter = filter;
|
||||
ofd.InitialDirectory = initialDirectory;
|
||||
if (ofd.ShowDialog() == DialogResult.OK)
|
||||
return ofd.FileName;
|
||||
return null;
|
||||
|
@ -116,7 +116,6 @@
|
||||
<Compile Include="Downloaders\MEGADownloader.cs" />
|
||||
<Compile Include="Downloaders\ModDBDownloader.cs" />
|
||||
<Compile Include="Downloaders\NexusDownloader.cs" />
|
||||
<Compile Include="Extensions\TaskExt.cs" />
|
||||
<Compile Include="Installer.cs" />
|
||||
<Compile Include="ModListRegistry\ModListMetadata.cs" />
|
||||
<Compile Include="NexusApi\Dtos.cs" />
|
||||
|
@ -31,18 +31,6 @@ namespace Wabbajack
|
||||
return This.WhenAny(property1, selector: x => x.GetValue());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convenience function that discards events that are null
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="source"></param>
|
||||
/// <returns>Source events that are not null</returns>
|
||||
public static IObservable<T> NotNull<T>(this IObservable<T> source)
|
||||
where T : class
|
||||
{
|
||||
return source.Where(u => u != null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convenience wrapper to observe following calls on the GUI thread.
|
||||
/// </summary>
|
||||
@ -51,42 +39,6 @@ namespace Wabbajack
|
||||
return source.ObserveOn(RxApp.MainThreadScheduler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts any observable to type Unit. Useful for when you care that a signal occurred,
|
||||
/// but don't care about what its value is downstream.
|
||||
/// </summary>
|
||||
/// <returns>An observable that returns Unit anytime the source signal fires an event.</returns>
|
||||
public static IObservable<Unit> Unit<T>(this IObservable<T> source)
|
||||
{
|
||||
return source.Select(_ => System.Reactive.Unit.Default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convenience operator to subscribe to the source observable, only when a second "switch" observable is on.
|
||||
/// When the switch is on, the source will be subscribed to, and its updates passed through.
|
||||
/// When the switch is off, the subscription to the source observable will be stopped, and no signal will be published.
|
||||
/// </summary>
|
||||
/// <param name="source">Source observable to subscribe to if on</param>
|
||||
/// <param name="filterSwitch">On/Off signal of whether to subscribe to source observable</param>
|
||||
/// <returns>Observable that publishes data from source, if the switch is on.</returns>
|
||||
public static IObservable<T> FilterSwitch<T>(this IObservable<T> source, IObservable<bool> filterSwitch)
|
||||
{
|
||||
return filterSwitch
|
||||
.DistinctUntilChanged()
|
||||
.Select(on =>
|
||||
{
|
||||
if (on)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Observable.Empty<T>();
|
||||
}
|
||||
})
|
||||
.Switch();
|
||||
}
|
||||
|
||||
public static IObservable<Unit> StartingExecution(this IReactiveCommand cmd)
|
||||
{
|
||||
return cmd.IsExecuting
|
||||
@ -95,114 +47,6 @@ namespace Wabbajack
|
||||
.Unit();
|
||||
}
|
||||
|
||||
/// Inspiration:
|
||||
/// http://reactivex.io/documentation/operators/debounce.html
|
||||
/// https://stackoverflow.com/questions/20034476/how-can-i-use-reactive-extensions-to-throttle-events-using-a-max-window-size
|
||||
public static IObservable<T> Debounce<T>(this IObservable<T> source, TimeSpan interval, IScheduler scheduler = null)
|
||||
{
|
||||
scheduler = scheduler ?? Scheduler.Default;
|
||||
return Observable.Create<T>(o =>
|
||||
{
|
||||
var hasValue = false;
|
||||
bool throttling = false;
|
||||
T value = default;
|
||||
|
||||
var dueTimeDisposable = new SerialDisposable();
|
||||
|
||||
void internalCallback()
|
||||
{
|
||||
if (hasValue)
|
||||
{
|
||||
// We have another value that came in to fire.
|
||||
// Reregister for callback
|
||||
dueTimeDisposable.Disposable = scheduler.Schedule(interval, internalCallback);
|
||||
o.OnNext(value);
|
||||
value = default;
|
||||
hasValue = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Nothing to do, throttle is complete.
|
||||
throttling = false;
|
||||
}
|
||||
}
|
||||
|
||||
return source.Subscribe(
|
||||
onNext: (x) =>
|
||||
{
|
||||
if (!throttling)
|
||||
{
|
||||
// Fire initial value
|
||||
o.OnNext(x);
|
||||
// Mark that we're throttling
|
||||
throttling = true;
|
||||
// Register for callback when throttle is complete
|
||||
dueTimeDisposable.Disposable = scheduler.Schedule(interval, internalCallback);
|
||||
}
|
||||
else
|
||||
{
|
||||
// In the middle of throttle
|
||||
// Save value and return
|
||||
hasValue = true;
|
||||
value = x;
|
||||
}
|
||||
},
|
||||
onError: o.OnError,
|
||||
onCompleted: o.OnCompleted);
|
||||
});
|
||||
}
|
||||
|
||||
public static IObservable<Unit> SelectTask<T>(this IObservable<T> source, Func<T, Task> task)
|
||||
{
|
||||
return source
|
||||
.SelectMany(async i =>
|
||||
{
|
||||
await task(i).ConfigureAwait(false);
|
||||
return System.Reactive.Unit.Default;
|
||||
});
|
||||
}
|
||||
|
||||
public static IObservable<Unit> SelectTask<T>(this IObservable<T> source, Func<Task> task)
|
||||
{
|
||||
return source
|
||||
.SelectMany(async _ =>
|
||||
{
|
||||
await task().ConfigureAwait(false);
|
||||
return System.Reactive.Unit.Default;
|
||||
});
|
||||
}
|
||||
|
||||
public static IObservable<R> SelectTask<T, R>(this IObservable<T> source, Func<Task<R>> task)
|
||||
{
|
||||
return source
|
||||
.SelectMany(_ => task());
|
||||
}
|
||||
|
||||
public static IObservable<R> SelectTask<T, R>(this IObservable<T> source, Func<T, Task<R>> task)
|
||||
{
|
||||
return source
|
||||
.SelectMany(x => task(x));
|
||||
}
|
||||
|
||||
public static IObservable<T> DoTask<T>(this IObservable<T> source, Func<T, Task> task)
|
||||
{
|
||||
return source
|
||||
.SelectMany(async (x) =>
|
||||
{
|
||||
await task(x).ConfigureAwait(false);
|
||||
return x;
|
||||
});
|
||||
}
|
||||
|
||||
public static IObservable<R> WhereCastable<T, R>(this IObservable<T> source)
|
||||
where R : class
|
||||
where T : class
|
||||
{
|
||||
return source
|
||||
.Select(x => x as R)
|
||||
.NotNull();
|
||||
}
|
||||
|
||||
/// These snippets were provided by RolandPheasant (author of DynamicData)
|
||||
/// They'll be going into the official library at some point, but are here for now.
|
||||
#region Dynamic Data EnsureUniqueChanges
|
||||
|
70
Wabbajack/Settings.cs
Normal file
70
Wabbajack/Settings.cs
Normal file
@ -0,0 +1,70 @@
|
||||
using Newtonsoft.Json;
|
||||
using ReactiveUI.Fody.Helpers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
[JsonObject(MemberSerialization.OptOut)]
|
||||
public class MainSettings
|
||||
{
|
||||
private static string Filename = "settings.json";
|
||||
|
||||
public double PosX { get; set; }
|
||||
public double PosY { get; set; }
|
||||
public double Height { get; set; }
|
||||
public double Width { get; set; }
|
||||
public string LastInstalledListLocation { get; set; }
|
||||
public Dictionary<string, InstallationSettings> InstallationSettings { get; } = new Dictionary<string, InstallationSettings>();
|
||||
public string LastCompiledProfileLocation { get; set; }
|
||||
public Dictionary<string, CompilationSettings> CompilationSettings { get; } = new Dictionary<string, CompilationSettings>();
|
||||
|
||||
[JsonIgnoreAttribute]
|
||||
private Subject<Unit> _saveSignal = new Subject<Unit>();
|
||||
public IObservable<Unit> SaveSignal => _saveSignal;
|
||||
|
||||
public static MainSettings LoadSettings()
|
||||
{
|
||||
if (!File.Exists(Filename)) return new MainSettings();
|
||||
return JsonConvert.DeserializeObject<MainSettings>(File.ReadAllText(Filename));
|
||||
}
|
||||
|
||||
public static void 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;
|
||||
|
||||
File.WriteAllText(Filename, JsonConvert.SerializeObject(settings, Formatting.Indented));
|
||||
}
|
||||
}
|
||||
|
||||
public class InstallationSettings
|
||||
{
|
||||
public string InstallationLocation { get; set; }
|
||||
public string DownloadLocation { get; set; }
|
||||
}
|
||||
|
||||
public class CompilationSettings
|
||||
{
|
||||
public string ModListName { get; set; }
|
||||
public string Author { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Website { get; set; }
|
||||
public string Readme { get; set; }
|
||||
public string SplashScreen { get; set; }
|
||||
public string Location { get; set; }
|
||||
public string DownloadLocation { get; set; }
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:Wabbajack"
|
||||
xmlns:mahapps="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
|
||||
xmlns:darkBlendTheme="clr-namespace:DarkBlendTheme"
|
||||
mc:Ignorable="d">
|
||||
|
||||
@ -838,49 +839,120 @@
|
||||
</Style>
|
||||
|
||||
<!-- TextBox -->
|
||||
<Style x:Key="MainTextBoxStyle" BasedOn="{StaticResource {x:Type Control}}" TargetType="{x:Type TextBox}">
|
||||
<Setter Property="Background" Value="{StaticResource TextBoxBackground}"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource TextBoxBorder}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="2,2,2,1"/>
|
||||
<Setter Property="AllowDrop" Value="true"/>
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||
<Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst"/>
|
||||
<Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
|
||||
<Style
|
||||
x:Key="MainTextBoxStyle"
|
||||
BasedOn="{StaticResource {x:Type Control}}"
|
||||
TargetType="{x:Type TextBox}">
|
||||
<Setter Property="Background" Value="{StaticResource TextBoxBackground}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource TextBoxBorder}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Padding" Value="2,2,2,1" />
|
||||
<Setter Property="AllowDrop" Value="true" />
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
|
||||
<Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst" />
|
||||
<Setter Property="Stylus.IsFlicksEnabled" Value="False" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type TextBox}">
|
||||
<Border x:Name="Bd"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Background="{TemplateBinding Background}"
|
||||
CornerRadius="3"
|
||||
SnapsToDevicePixels="true">
|
||||
<ScrollViewer x:Name="PART_ContentHost" Padding="{TemplateBinding Padding}"
|
||||
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
|
||||
<Border
|
||||
x:Name="Bd"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="3"
|
||||
SnapsToDevicePixels="true">
|
||||
<Grid>
|
||||
<ScrollViewer
|
||||
x:Name="PART_ContentHost"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
|
||||
<TextBlock
|
||||
x:Name="PART_Message"
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
Foreground="{TemplateBinding Foreground}"
|
||||
Style="{DynamicResource MahApps.Metro.Styles.MetroWatermarkTextBlock}"
|
||||
Text="{TemplateBinding mahapps:TextBoxHelper.Watermark}"
|
||||
TextAlignment="{TemplateBinding mahapps:TextBoxHelper.WatermarkAlignment}"
|
||||
TextTrimming="{TemplateBinding mahapps:TextBoxHelper.WatermarkTrimming}"
|
||||
TextWrapping="{TemplateBinding mahapps:TextBoxHelper.WatermarkWrapping}"
|
||||
Visibility="Collapsed" />
|
||||
</Grid>
|
||||
</Border>
|
||||
<ControlTemplate.Resources>
|
||||
<Storyboard x:Key="enterGotFocus">
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="PART_Message"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
To=".2"
|
||||
Duration="0:0:0.2" />
|
||||
</Storyboard>
|
||||
<Storyboard x:Key="exitGotFocus">
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="PART_Message"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
Duration="0:0:0.2" />
|
||||
</Storyboard>
|
||||
|
||||
<Storyboard x:Key="enterHasText">
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="PART_Message"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
From=".2"
|
||||
To="0"
|
||||
Duration="0:0:0.2" />
|
||||
</Storyboard>
|
||||
<Storyboard x:Key="exitHasText">
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="PART_Message"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
Duration="0:0:0.2" />
|
||||
</Storyboard>
|
||||
</ControlTemplate.Resources>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsEnabled" Value="false">
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBoxDisabledForeground}"/>
|
||||
<Setter Property="Background" TargetName="PART_ContentHost" Value="{StaticResource TextBoxDisabledBackground}"/>
|
||||
<Setter Property="Background" TargetName="Bd" Value="{StaticResource TextBoxDisabledBackground}"/>
|
||||
<Setter Property="BorderBrush" TargetName="Bd" Value="{StaticResource TextBoxDisabledBackground}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBoxDisabledForeground}" />
|
||||
<Setter TargetName="PART_ContentHost" Property="Background" Value="{StaticResource TextBoxDisabledBackground}" />
|
||||
<Setter TargetName="Bd" Property="Background" Value="{StaticResource TextBoxDisabledBackground}" />
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource TextBoxDisabledBackground}" />
|
||||
</Trigger>
|
||||
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="mahapps:TextBoxHelper.HasText" Value="False" />
|
||||
<Condition Property="IsFocused" Value="True" />
|
||||
</MultiTrigger.Conditions>
|
||||
<MultiTrigger.EnterActions>
|
||||
<BeginStoryboard Storyboard="{StaticResource enterGotFocus}" />
|
||||
</MultiTrigger.EnterActions>
|
||||
<MultiTrigger.ExitActions>
|
||||
<BeginStoryboard Storyboard="{StaticResource exitGotFocus}" />
|
||||
</MultiTrigger.ExitActions>
|
||||
</MultiTrigger>
|
||||
|
||||
<Trigger Property="mahapps:TextBoxHelper.HasText" Value="True">
|
||||
<Trigger.EnterActions>
|
||||
<BeginStoryboard Storyboard="{StaticResource enterHasText}" />
|
||||
</Trigger.EnterActions>
|
||||
<Trigger.ExitActions>
|
||||
<BeginStoryboard Storyboard="{StaticResource exitHasText}" />
|
||||
</Trigger.ExitActions>
|
||||
</Trigger>
|
||||
|
||||
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=Text}" Value="">
|
||||
<Setter TargetName="PART_Message" Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Path=(Validation.HasError), RelativeSource={RelativeSource self}}" Value="True">
|
||||
<Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
|
||||
<Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}" />
|
||||
</DataTrigger>
|
||||
<!--<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="IsInactiveSelectionHighlightEnabled" Value="true"/>
|
||||
<Condition Property="IsSelectionActive" Value="false"/>
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter Property="SelectionBrush" Value="{StaticResource InactiveSelectionHighlightBrush}"/>
|
||||
</MultiTrigger>-->
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
<Style BasedOn="{StaticResource MainTextBoxStyle}" TargetType="TextBox" />
|
||||
|
@ -1,15 +1,10 @@
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.Fody.Helpers;
|
||||
using Splat;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Wabbajack.Common;
|
||||
@ -49,7 +44,7 @@ namespace Wabbajack
|
||||
public BitmapImage Image => _Image.Value;
|
||||
|
||||
[Reactive]
|
||||
public string NexusSiteURL { get; set; }
|
||||
public string Website { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public string ReadMeText { get; set; }
|
||||
@ -85,6 +80,36 @@ namespace Wabbajack
|
||||
.ToProperty(this, nameof(this.Image));
|
||||
|
||||
ConfigureForBuild(source);
|
||||
|
||||
// Load settings
|
||||
CompilationSettings settings = this.MWVM.Settings.CompilationSettings.TryCreate(source);
|
||||
this.AuthorName = settings.Author;
|
||||
this.ModListName = settings.ModListName;
|
||||
this.Summary = settings.Description;
|
||||
this.ReadMeText = settings.Readme;
|
||||
this.ImagePath = settings.SplashScreen;
|
||||
this.Website = settings.Website;
|
||||
if (!string.IsNullOrWhiteSpace(settings.DownloadLocation))
|
||||
{
|
||||
this.DownloadLocation = settings.DownloadLocation;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(settings.Location))
|
||||
{
|
||||
this.Location = settings.Location;
|
||||
}
|
||||
this.MWVM.Settings.SaveSignal
|
||||
.Subscribe(_ =>
|
||||
{
|
||||
settings.Author = this.AuthorName;
|
||||
settings.ModListName = this.ModListName;
|
||||
settings.Description = this.Summary;
|
||||
settings.Readme = this.ReadMeText;
|
||||
settings.SplashScreen = this.ImagePath;
|
||||
settings.Website = this.Website;
|
||||
settings.Location = this.Location;
|
||||
settings.DownloadLocation = this.DownloadLocation;
|
||||
})
|
||||
.DisposeWith(this.CompositeDisposable);
|
||||
}
|
||||
|
||||
private void ConfigureForBuild(string location)
|
||||
@ -93,7 +118,7 @@ namespace Wabbajack
|
||||
this.Mo2Folder = Path.GetDirectoryName(Path.GetDirectoryName(profile_folder));
|
||||
if (!File.Exists(Path.Combine(this.Mo2Folder, "ModOrganizer.exe")))
|
||||
{
|
||||
this.Log().Error($"Error! No ModOrganizer2.exe found in {this.Mo2Folder}");
|
||||
Utils.Log($"Error! No ModOrganizer2.exe found in {this.Mo2Folder}");
|
||||
}
|
||||
|
||||
this.MOProfile = Path.GetFileName(profile_folder);
|
||||
@ -114,7 +139,7 @@ namespace Wabbajack
|
||||
ModListAuthor = this.AuthorName,
|
||||
ModListDescription = this.Summary,
|
||||
ModListImage = this.ImagePath,
|
||||
ModListWebsite = this.NexusSiteURL,
|
||||
ModListWebsite = this.Website,
|
||||
ModListReadme = this.ReadMeText,
|
||||
};
|
||||
await Task.Run(() =>
|
||||
@ -131,7 +156,7 @@ namespace Wabbajack
|
||||
catch (Exception ex)
|
||||
{
|
||||
while (ex.InnerException != null) ex = ex.InnerException;
|
||||
this.Log().Warn(ex, "Can't continue");
|
||||
Utils.Log($"Can't continue: {ex.ExceptionToString()}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -141,7 +166,7 @@ namespace Wabbajack
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Log().Warn("Cannot compile modlist: no valid Mod Organizer profile directory selected.");
|
||||
Utils.Log("Cannot compile modlist: no valid Mod Organizer profile directory selected.");
|
||||
UIReady = true;
|
||||
}
|
||||
}
|
||||
|
@ -1,32 +1,18 @@
|
||||
using Syroot.Windows.IO;
|
||||
using System;
|
||||
using ReactiveUI;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Threading;
|
||||
using Wabbajack.Common;
|
||||
using Wabbajack.Lib.Downloaders;
|
||||
using Wabbajack.Lib.NexusApi;
|
||||
using DynamicData;
|
||||
using DynamicData.Binding;
|
||||
using System.Reactive;
|
||||
using System.Text;
|
||||
using Wabbajack.Lib;
|
||||
using Splat;
|
||||
using ReactiveUI.Fody.Helpers;
|
||||
|
||||
namespace Wabbajack
|
||||
@ -96,7 +82,7 @@ namespace Wabbajack
|
||||
public IReactiveCommand OpenReadmeCommand { get; }
|
||||
public IReactiveCommand VisitWebsiteCommand { get; }
|
||||
|
||||
public InstallerVM(MainWindowVM mainWindowVM)
|
||||
public InstallerVM(MainWindowVM mainWindowVM, string source)
|
||||
{
|
||||
if (Path.GetDirectoryName(Assembly.GetEntryAssembly().Location.ToLower()) == KnownFolders.Downloads.Path.ToLower())
|
||||
{
|
||||
@ -110,13 +96,26 @@ namespace Wabbajack
|
||||
}
|
||||
|
||||
this.MWVM = mainWindowVM;
|
||||
this.ModListPath = source;
|
||||
|
||||
// Load settings
|
||||
InstallationSettings settings = this.MWVM.Settings.InstallationSettings.TryCreate(source);
|
||||
this.Location = settings.InstallationLocation;
|
||||
this.DownloadLocation = settings.DownloadLocation;
|
||||
this.MWVM.Settings.SaveSignal
|
||||
.Subscribe(_ =>
|
||||
{
|
||||
settings.InstallationLocation = this.Location;
|
||||
settings.DownloadLocation = this.DownloadLocation;
|
||||
})
|
||||
.DisposeWith(this.CompositeDisposable);
|
||||
|
||||
this._ModList = this.WhenAny(x => x.ModListPath)
|
||||
.ObserveOn(RxApp.TaskpoolScheduler)
|
||||
.Select(source =>
|
||||
.Select(modListPath =>
|
||||
{
|
||||
if (source == null) return default(ModListVM);
|
||||
var modList = Installer.LoadFromFile(source);
|
||||
if (modListPath == null) return default(ModListVM);
|
||||
var modList = Installer.LoadFromFile(modListPath);
|
||||
if (modList == null)
|
||||
{
|
||||
MessageBox.Show("Invalid Modlist, or file not found.", "Invalid Modlist", MessageBoxButton.OK,
|
||||
@ -133,7 +132,7 @@ namespace Wabbajack
|
||||
});
|
||||
return default(ModListVM);
|
||||
}
|
||||
return new ModListVM(modList, source);
|
||||
return new ModListVM(modList, modListPath);
|
||||
})
|
||||
.ObserveOnGuiThread()
|
||||
.StartWith(default(ModListVM))
|
||||
|
@ -26,6 +26,8 @@ namespace Wabbajack
|
||||
{
|
||||
public MainWindow MainWindow { get; }
|
||||
|
||||
public MainSettings Settings { get; }
|
||||
|
||||
private readonly ObservableAsPropertyHelper<ViewModel> _ActivePane;
|
||||
public ViewModel ActivePane => _ActivePane.Value;
|
||||
|
||||
@ -34,7 +36,6 @@ namespace Wabbajack
|
||||
|
||||
public ObservableCollectionExtended<CPUStatus> StatusList { get; } = new ObservableCollectionExtended<CPUStatus>();
|
||||
|
||||
private Subject<string> _logSubj = new Subject<string>();
|
||||
public ObservableCollectionExtended<string> Log { get; } = new ObservableCollectionExtended<string>();
|
||||
|
||||
[Reactive]
|
||||
@ -43,11 +44,12 @@ namespace Wabbajack
|
||||
private readonly Lazy<CompilerVM> _Compiler;
|
||||
private readonly Lazy<InstallerVM> _Installer;
|
||||
|
||||
public MainWindowVM(RunMode mode, string source, MainWindow mainWindow)
|
||||
public MainWindowVM(RunMode mode, string source, MainWindow mainWindow, MainSettings settings)
|
||||
{
|
||||
this.Mode = mode;
|
||||
this.MainWindow = mainWindow;
|
||||
this._Installer = new Lazy<InstallerVM>(() => new InstallerVM(this));
|
||||
this.Settings = settings;
|
||||
this._Installer = new Lazy<InstallerVM>(() => new InstallerVM(this, source));
|
||||
this._Compiler = new Lazy<CompilerVM>(() => new CompilerVM(this, source));
|
||||
|
||||
// Set up logging
|
||||
@ -82,11 +84,6 @@ namespace Wabbajack
|
||||
}
|
||||
})
|
||||
.ToProperty(this, nameof(this.ActivePane));
|
||||
this.WhenAny(x => x.ActivePane)
|
||||
.ObserveOn(RxApp.TaskpoolScheduler)
|
||||
.WhereCastable<ViewModel, InstallerVM>()
|
||||
.Subscribe(vm => vm.ModListPath = source)
|
||||
.DisposeWith(this.CompositeDisposable);
|
||||
|
||||
// Compile progress updates and populate ObservableCollection
|
||||
WorkQueue.Status
|
||||
|
@ -4,6 +4,7 @@
|
||||
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"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
@ -99,6 +100,7 @@
|
||||
<TextBlock Margin="{StaticResource TitleMargin}" Text="Description" />
|
||||
<TextBox
|
||||
Height="150"
|
||||
mahapps:TextBoxHelper.Watermark="(700 characters max)"
|
||||
AcceptsReturn="True"
|
||||
AcceptsTab="False"
|
||||
MaxLength="700"
|
||||
@ -106,7 +108,7 @@
|
||||
Text="{Binding Summary}"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock Margin="{StaticResource TitleMargin}" Text="Website" />
|
||||
<TextBox Style="{StaticResource ValueStyle}" Text="{Binding NexusSiteURL}" />
|
||||
<TextBox Style="{StaticResource ValueStyle}" Text="{Binding Website}" />
|
||||
<TextBlock
|
||||
Margin="{StaticResource TitleMargin}"
|
||||
Text="Readme Path"
|
||||
|
@ -193,7 +193,7 @@ namespace Wabbajack
|
||||
this._InError = Observable.CombineLatest(
|
||||
this.WhenAny(x => x.Exists),
|
||||
this.WhenAny(x => x.AdditionalError)
|
||||
.Select(err => !err?.Succeeded ?? true),
|
||||
.Select(err => !err?.Succeeded ?? false),
|
||||
resultSelector: (exist, err) => !exist || err)
|
||||
.ToProperty(this, nameof(this.InError));
|
||||
|
||||
|
@ -13,6 +13,7 @@ namespace Wabbajack
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private MainWindowVM _mwvm;
|
||||
private MainSettings _settings;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
@ -20,19 +21,20 @@ namespace Wabbajack
|
||||
|
||||
if (args.Length != 3) return;
|
||||
var modlistPath = args[2];
|
||||
Initialize(RunMode.Install, modlistPath);
|
||||
this._settings = MainSettings.LoadSettings();
|
||||
Initialize(RunMode.Install, modlistPath, this._settings);
|
||||
}
|
||||
|
||||
public MainWindow(RunMode mode, string source)
|
||||
public MainWindow(RunMode mode, string source, MainSettings settings)
|
||||
{
|
||||
Initialize(mode, source);
|
||||
Initialize(mode, source, settings);
|
||||
}
|
||||
|
||||
private void Initialize(RunMode mode, string source)
|
||||
private void Initialize(RunMode mode, string source, MainSettings settings)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_mwvm = new MainWindowVM(mode, source, this);
|
||||
this._settings = settings;
|
||||
_mwvm = new MainWindowVM(mode, source, this, settings);
|
||||
Utils.Log($"Wabbajack Build - {ThisAssembly.Git.Sha}");
|
||||
this.DataContext = _mwvm;
|
||||
}
|
||||
@ -42,6 +44,7 @@ namespace Wabbajack
|
||||
private void Window_Closing(object sender, CancelEventArgs e)
|
||||
{
|
||||
_mwvm.Dispose();
|
||||
MainSettings.SaveSettings(this._settings);
|
||||
if (ExitWhenClosing)
|
||||
{
|
||||
Application.Current.Shutdown();
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using Wabbajack.Common;
|
||||
@ -16,7 +17,7 @@ namespace Wabbajack
|
||||
/// </summary>
|
||||
public partial class ModeSelectionWindow : Window
|
||||
{
|
||||
private List<ModlistMetadata> _lists;
|
||||
MainSettings settings;
|
||||
|
||||
public ModeSelectionWindow()
|
||||
{
|
||||
@ -30,6 +31,7 @@ namespace Wabbajack
|
||||
var discordIcon = UIUtils.BitmapImageFromResource("Wabbajack.Resources.Icons.discord.png");
|
||||
Discord.Source = discordIcon;
|
||||
|
||||
settings = MainSettings.LoadSettings();
|
||||
DataContext = new ModeSelectionWindowVM();
|
||||
}
|
||||
|
||||
@ -37,7 +39,9 @@ namespace Wabbajack
|
||||
{
|
||||
OpenMainWindow(
|
||||
RunMode.Compile,
|
||||
UIUtils.OpenFileDialog("MO2 Modlist(modlist.txt)|modlist.txt"));
|
||||
UIUtils.OpenFileDialog(
|
||||
"MO2 Modlist(modlist.txt)|modlist.txt",
|
||||
initialDirectory: settings.LastCompiledProfileLocation));
|
||||
}
|
||||
|
||||
private void InstallModlist_Click(object sender, RoutedEventArgs e)
|
||||
@ -57,7 +61,18 @@ namespace Wabbajack
|
||||
{
|
||||
if (file == null) return;
|
||||
ShutdownOnClose = false;
|
||||
var window = new MainWindow(mode, file);
|
||||
switch (mode)
|
||||
{
|
||||
case RunMode.Compile:
|
||||
settings.LastCompiledProfileLocation = Path.GetDirectoryName(file);
|
||||
break;
|
||||
case RunMode.Install:
|
||||
settings.LastInstalledListLocation = Path.GetDirectoryName(file);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
var window = new MainWindow(mode, file, settings);
|
||||
window.Left = this.Left;
|
||||
window.Top = this.Top;
|
||||
window.Show();
|
||||
@ -90,7 +105,9 @@ namespace Wabbajack
|
||||
private void InstallFromList_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
OpenMainWindow(RunMode.Install,
|
||||
UIUtils.OpenFileDialog($"*{ExtensionManager.Extension}|*{ExtensionManager.Extension}"));
|
||||
UIUtils.OpenFileDialog(
|
||||
$"*{ExtensionManager.Extension}|*{ExtensionManager.Extension}",
|
||||
initialDirectory: settings.LastInstalledListLocation));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -163,6 +163,7 @@
|
||||
</ApplicationDefinition>
|
||||
<Compile Include="Converters\BoolToVisibilityConverter.cs" />
|
||||
<Compile Include="Extensions\EnumerableExt.cs" />
|
||||
<Compile Include="Settings.cs" />
|
||||
<Compile Include="View Models\ModListVM.cs" />
|
||||
<Compile Include="View Models\ModVM.cs" />
|
||||
<Compile Include="Views\CompilerView.xaml.cs">
|
||||
|
Reference in New Issue
Block a user