Merge pull request #151 from Noggog/settings-file

Settings File
This commit is contained in:
Timothy Baldridge
2019-11-06 06:12:39 -07:00
committed by GitHub
17 changed files with 466 additions and 236 deletions

View 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;
}
}
}

View 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();
}
}
}

View File

@ -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" />

View File

@ -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;

View File

@ -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" />

View File

@ -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
View 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; }
}
}

View File

@ -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" />

View File

@ -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;
}
}

View File

@ -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))

View File

@ -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

View File

@ -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"

View File

@ -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));

View File

@ -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();

View File

@ -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));
}
}
}

View File

@ -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">