Merge pull request #274 from Noggog/polish-and-fixes

Polish and Fixes
This commit is contained in:
Timothy Baldridge 2019-12-15 16:09:44 -07:00 committed by GitHub
commit e9585feb9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1395 additions and 392 deletions

View File

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Wabbajack.Common
{
public class TempFile : IDisposable
{
public FileInfo File { get; private set; }
public bool DeleteAfter = true;
public TempFile(bool deleteAfter = true, bool createFolder = true)
: this(new FileInfo(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())))
{
}
public TempFile(FileInfo file, bool deleteAfter = true, bool createFolder = true)
{
this.File = file;
if (createFolder && !file.Directory.Exists)
{
file.Directory.Create();
}
this.DeleteAfter = deleteAfter;
}
public void Dispose()
{
if (DeleteAfter)
{
this.File.Delete();
}
}
}
}

View File

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Wabbajack.Common
{
public class TempFolder : IDisposable
{
public DirectoryInfo Dir { get; private set; }
public bool DeleteAfter = true;
public TempFolder(bool deleteAfter = true)
{
this.Dir = new DirectoryInfo(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()));
this.Dir.Create();
this.DeleteAfter = deleteAfter;
}
public TempFolder(DirectoryInfo dir, bool deleteAfter = true)
{
this.Dir = dir;
if (!dir.Exists)
{
this.Dir.Create();
}
this.DeleteAfter = deleteAfter;
}
public TempFolder(string addedFolderPath, bool deleteAfter = true)
: this(new DirectoryInfo(Path.Combine(Path.GetTempPath(), addedFolderPath)), deleteAfter: deleteAfter)
{
}
public void Dispose()
{
if (DeleteAfter)
{
Utils.DeleteDirectory(this.Dir.FullName);
}
}
}
}

View File

@ -75,9 +75,6 @@
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<Reference Include="Syroot.KnownFolders">
<HintPath>..\..\..\Users\tbald\.nuget\packages\syroot.windows.io.knownfolders\1.2.1\lib\net452\Syroot.KnownFolders.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
@ -103,6 +100,7 @@
<Compile Include="Enums\ModManager.cs" />
<Compile Include="ExtensionManager.cs" />
<Compile Include="Extensions\DictionaryExt.cs" />
<Compile Include="Extensions\EnumerableExt.cs" />
<Compile Include="Extensions\EnumExt.cs" />
<Compile Include="Extensions\HashHelper.cs" />
<Compile Include="Extensions\RxExt.cs" />
@ -131,6 +129,8 @@
<Compile Include="StatusUpdate.cs" />
<Compile Include="SteamHandler.cs" />
<Compile Include="Utils.cs" />
<Compile Include="Util\TempFile.cs" />
<Compile Include="Util\TempFolder.cs" />
<Compile Include="WorkQueue.cs" />
</ItemGroup>
<ItemGroup>

View File

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

View File

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

View File

@ -1,4 +1,5 @@
using Microsoft.WindowsAPICodePack.Dialogs;
using DynamicData;
using Microsoft.WindowsAPICodePack.Dialogs;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using System;
@ -9,7 +10,7 @@ using System.Reactive.Linq;
using System.Windows.Input;
using Wabbajack.Lib;
namespace Wabbajack
namespace Wabbajack.Lib
{
public class FilePickerVM : ViewModel
{
@ -21,10 +22,10 @@ namespace Wabbajack
Folder
}
public enum ExistCheckOptions
public enum CheckOptions
{
Off,
IfNotEmpty,
IfPathNotEmpty,
On
}
@ -43,7 +44,10 @@ namespace Wabbajack
public PathTypeOptions PathType { get; set; }
[Reactive]
public ExistCheckOptions ExistCheckOption { get; set; }
public CheckOptions ExistCheckOption { get; set; }
[Reactive]
public CheckOptions FilterCheckOption { get; set; } = CheckOptions.IfPathNotEmpty;
[Reactive]
public IObservable<IErrorResponse> AdditionalError { get; set; }
@ -60,46 +64,52 @@ namespace Wabbajack
private readonly ObservableAsPropertyHelper<string> _errorTooltip;
public string ErrorTooltip => _errorTooltip.Value;
public List<CommonFileDialogFilter> Filters { get; } = new List<CommonFileDialogFilter>();
public SourceList<CommonFileDialogFilter> Filters { get; } = new SourceList<CommonFileDialogFilter>();
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();
// Check that file exists
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))
.StartWith(default(string)),
// Dont want to debounce the initial value, because we know it's null
.Skip(1)
.Debounce(TimeSpan.FromMilliseconds(200))
.StartWith(default(string)),
resultSelector: (existsOption, type, path) => (ExistsOption: existsOption, Type: type, Path: path))
.Publish()
.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 !string.IsNullOrWhiteSpace(t.Path);
case CheckOptions.On:
return true;
default:
throw new NotImplementedException();
}
})
.Replay(1)
.RefCount();
_exists = Observable.Interval(TimeSpan.FromSeconds(3))
// Only check exists on timer if desired
.FilterSwitch(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 ExistCheckOptions.Off:
return false;
case ExistCheckOptions.IfNotEmpty:
return !string.IsNullOrWhiteSpace(t.Path);
case ExistCheckOptions.On:
return true;
default:
throw new NotImplementedException();
}
}))
.FilterSwitch(doExistsCheck)
.Unit()
// Also check though, when fields change
.Merge(this.WhenAny(x => x.PathType).Unit())
@ -113,14 +123,14 @@ namespace Wabbajack
{
switch (t.ExistsOption)
{
case ExistCheckOptions.IfNotEmpty:
if (string.IsNullOrWhiteSpace(t.Path)) return true;
case CheckOptions.IfPathNotEmpty:
if (string.IsNullOrWhiteSpace(t.Path)) return false;
break;
case ExistCheckOptions.On:
case CheckOptions.On:
break;
case ExistCheckOptions.Off:
case CheckOptions.Off:
default:
return true;
return false;
}
switch (t.Type)
{
@ -130,24 +140,81 @@ namespace Wabbajack
return File.Exists(t.Path);
case PathTypeOptions.Folder:
return Directory.Exists(t.Path);
case PathTypeOptions.Off:
default:
return false;
}
})
.DistinctUntilChanged()
.ObserveOn(RxApp.MainThreadScheduler)
.StartWith(false)
.ToProperty(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) =>
{
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 (string.IsNullOrWhiteSpace(target)) return true;
break;
case CheckOptions.On:
break;
default:
throw new NotImplementedException();
}
try
{
var extension = Path.GetExtension(target);
if (extension == null || !extension.StartsWith(".")) return false;
extension = extension.Substring(1);
if (!query.Any(filter => filter.Extensions.Any(ext => string.Equals(ext, extension)))) return false;
}
catch (ArgumentException)
{
return false;
}
return true;
})
.StartWith(false)
.DistinctUntilChanged()
.ObserveOn(RxApp.MainThreadScheduler)
.ToProperty(this, nameof(Exists));
.StartWith(true)
.Select(passed =>
{
if (passed) return ErrorResponse.Success;
return ErrorResponse.Fail(DoesNotPassFiltersText);
})
.Replay(1)
.RefCount();
_errorState = Observable.CombineLatest(
this.WhenAny(x => x.Exists)
.Select(exists => ErrorResponse.Create(successful: exists, exists ? default(string) : "Path does not exist")),
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: (exist, err) =>
resultSelector: (existCheck, filter, err) =>
{
if (exist.Failed) return exist;
if (existCheck.Failed) return existCheck;
if (filter.Failed) return filter;
return ErrorResponse.Convert(err);
})
.ToProperty(this, nameof(ErrorState));
@ -159,14 +226,20 @@ namespace Wabbajack
// Doesn't derive from ErrorState, as we want to bubble non-empty tooltips,
// which is slightly different logic
_errorTooltip = Observable.CombineLatest(
this.WhenAny(x => x.Exists)
.Select(exists => exists ? default(string) : "Path does not exist"),
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, err) =>
resultSelector: (exists, filters, err) =>
{
if (!string.IsNullOrWhiteSpace(exists)) return exists;
if (!string.IsNullOrWhiteSpace(filters)) return filters;
return err?.Reason;
})
.ToProperty(this, nameof(ErrorTooltip));
@ -201,7 +274,7 @@ namespace Wabbajack
Multiselect = false,
ShowPlacesList = true,
};
foreach (var filter in Filters)
foreach (var filter in Filters.Items)
{
dlg.Filters.Add(filter);
}

View File

@ -18,8 +18,10 @@ namespace Wabbajack.Lib
{
var img = new BitmapImage();
img.BeginInit();
img.CacheOption = BitmapCacheOption.OnLoad;
img.StreamSource = stream;
img.EndInit();
img.Freeze();
return img;
}

View File

@ -59,7 +59,6 @@
<ItemGroup>
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="Syroot.KnownFolders" />
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Design" />
@ -119,6 +118,7 @@
<Compile Include="Downloaders\GameFileSourceDownloader.cs" />
<Compile Include="Downloaders\LoversLabDownloader.cs" />
<Compile Include="Downloaders\SteamWorkshopDownloader.cs" />
<Compile Include="Extensions\ReactiveUIExt.cs" />
<Compile Include="LibCefHelpers\Init.cs" />
<Compile Include="MO2Compiler.cs" />
<Compile Include="Data.cs" />
@ -145,6 +145,7 @@
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ReportBuilder.cs" />
<Compile Include="StatusMessages\ConfirmUpdateOfExistingInstall.cs" />
<Compile Include="UI\FilePickerVM.cs" />
<Compile Include="UI\UIUtils.cs" />
<Compile Include="Validation\DTOs.cs" />
<Compile Include="Validation\ValidateModlist.cs" />
@ -210,6 +211,9 @@
<PackageReference Include="ReactiveUI">
<Version>11.0.1</Version>
</PackageReference>
<PackageReference Include="ReactiveUI.Fody">
<Version>11.0.1</Version>
</PackageReference>
<PackageReference Include="SharpCompress">
<Version>0.24.0</Version>
</PackageReference>

View File

@ -0,0 +1,368 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using DynamicData;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Wabbajack.Common;
using Wabbajack.Lib;
namespace Wabbajack.Test
{
[TestClass]
public class FilePickerTests
{
public static TempFile CreateSetFile(FilePickerVM vm)
{
var temp = new TempFile();
using (new FileStream(temp.File.FullName, FileMode.CreateNew)) { }
vm.TargetPath = temp.File.FullName;
return temp;
}
public static TempFolder CreateSetFolder(FilePickerVM vm)
{
var temp = new TempFolder();
Directory.CreateDirectory(temp.Dir.FullName);
vm.TargetPath = temp.Dir.FullName;
return temp;
}
[TestMethod]
public async Task Stock()
{
var vm = new FilePickerVM();
Assert.AreEqual(FilePickerVM.PathTypeOptions.Off, vm.PathType);
Assert.AreEqual(FilePickerVM.CheckOptions.Off, vm.ExistCheckOption);
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
Assert.IsTrue(string.IsNullOrEmpty(vm.ErrorTooltip));
}
[TestMethod]
public async Task FileNoExistsCheck_DoesNotExist()
{
var vm = new FilePickerVM();
vm.PathType = FilePickerVM.PathTypeOptions.File;
vm.ExistCheckOption = FilePickerVM.CheckOptions.Off;
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
Assert.IsTrue(string.IsNullOrEmpty(vm.ErrorTooltip));
}
[TestMethod]
public async Task FileNoExistsCheck_Exists()
{
var vm = new FilePickerVM();
using (CreateSetFile(vm))
{
vm.PathType = FilePickerVM.PathTypeOptions.File;
vm.ExistCheckOption = FilePickerVM.CheckOptions.Off;
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
Assert.IsTrue(string.IsNullOrEmpty(vm.ErrorTooltip));
}
}
[TestMethod]
public async Task ExistCheckTypeOff_DoesNotExist()
{
var vm = new FilePickerVM();
vm.PathType = FilePickerVM.PathTypeOptions.Off;
vm.ExistCheckOption = FilePickerVM.CheckOptions.On;
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
Assert.IsTrue(string.IsNullOrEmpty(vm.ErrorTooltip));
}
[TestMethod]
public async Task ExistCheckTypeOff_Exists()
{
var vm = new FilePickerVM();
using (CreateSetFile(vm))
{
vm.PathType = FilePickerVM.PathTypeOptions.Off;
vm.ExistCheckOption = FilePickerVM.CheckOptions.On;
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
Assert.IsTrue(string.IsNullOrEmpty(vm.ErrorTooltip));
}
}
[TestMethod]
public async Task FileIfNotEmptyCheck_DoesNotExist()
{
var vm = new FilePickerVM();
vm.PathType = FilePickerVM.PathTypeOptions.File;
vm.ExistCheckOption = FilePickerVM.CheckOptions.IfPathNotEmpty;
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
Assert.IsTrue(string.IsNullOrEmpty(vm.ErrorTooltip));
}
[TestMethod]
public async Task FileIfNotEmptyCheck_SetPath_DoesNotExist()
{
var vm = new FilePickerVM();
vm.PathType = FilePickerVM.PathTypeOptions.File;
vm.TargetPath = "SomePath.jpg";
vm.ExistCheckOption = FilePickerVM.CheckOptions.IfPathNotEmpty;
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsFalse(vm.ErrorState.Succeeded);
Assert.IsTrue(vm.InError);
Assert.AreEqual(FilePickerVM.PathDoesNotExistText, vm.ErrorTooltip);
}
[TestMethod]
public async Task FileIfNotEmptyCheck_Exists()
{
var vm = new FilePickerVM();
using (CreateSetFile(vm))
{
vm.PathType = FilePickerVM.PathTypeOptions.File;
vm.ExistCheckOption = FilePickerVM.CheckOptions.IfPathNotEmpty;
await Task.Delay(250);
Assert.IsTrue(vm.Exists);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
Assert.IsTrue(string.IsNullOrEmpty(vm.ErrorTooltip));
}
}
[TestMethod]
public async Task FileOnExistsCheck_DoesNotExist()
{
var vm = new FilePickerVM();
vm.PathType = FilePickerVM.PathTypeOptions.File;
vm.ExistCheckOption = FilePickerVM.CheckOptions.On;
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsFalse(vm.ErrorState.Succeeded);
Assert.IsTrue(vm.InError);
Assert.AreEqual(FilePickerVM.PathDoesNotExistText, vm.ErrorTooltip);
}
[TestMethod]
public async Task FileOnExistsCheck_Exists()
{
var vm = new FilePickerVM();
using (CreateSetFile(vm))
{
vm.PathType = FilePickerVM.PathTypeOptions.File;
vm.ExistCheckOption = FilePickerVM.CheckOptions.On;
await Task.Delay(250);
Assert.IsTrue(vm.Exists);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
Assert.IsTrue(string.IsNullOrEmpty(vm.ErrorTooltip));
}
}
[TestMethod]
public async Task FolderIfNotEmptyCheck_DoesNotExist()
{
var vm = new FilePickerVM();
vm.PathType = FilePickerVM.PathTypeOptions.Folder;
vm.ExistCheckOption = FilePickerVM.CheckOptions.IfPathNotEmpty;
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
Assert.IsTrue(string.IsNullOrEmpty(vm.ErrorTooltip));
}
[TestMethod]
public async Task FolderIfNotEmptyCheck_SetPath_DoesNotExist()
{
var vm = new FilePickerVM();
vm.PathType = FilePickerVM.PathTypeOptions.Folder;
vm.TargetPath = "SomePath.jpg";
vm.ExistCheckOption = FilePickerVM.CheckOptions.IfPathNotEmpty;
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsFalse(vm.ErrorState.Succeeded);
Assert.IsTrue(vm.InError);
Assert.AreEqual(FilePickerVM.PathDoesNotExistText, vm.ErrorTooltip);
}
[TestMethod]
public async Task FolderIfNotEmptyCheck_Exists()
{
var vm = new FilePickerVM();
using (CreateSetFolder(vm))
{
vm.PathType = FilePickerVM.PathTypeOptions.Folder;
vm.ExistCheckOption = FilePickerVM.CheckOptions.IfPathNotEmpty;
await Task.Delay(250);
Assert.IsTrue(vm.Exists);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
Assert.IsTrue(string.IsNullOrEmpty(vm.ErrorTooltip));
}
}
[TestMethod]
public async Task FolderOnExistsCheck_DoesNotExist()
{
var vm = new FilePickerVM();
vm.PathType = FilePickerVM.PathTypeOptions.Folder;
vm.ExistCheckOption = FilePickerVM.CheckOptions.On;
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsFalse(vm.ErrorState.Succeeded);
Assert.IsTrue(vm.InError);
Assert.AreEqual(FilePickerVM.PathDoesNotExistText, vm.ErrorTooltip);
}
[TestMethod]
public async Task FolderOnExistsCheck_Exists()
{
var vm = new FilePickerVM();
using (CreateSetFolder(vm))
{
vm.PathType = FilePickerVM.PathTypeOptions.Folder;
vm.ExistCheckOption = FilePickerVM.CheckOptions.On;
await Task.Delay(250);
Assert.IsTrue(vm.Exists);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
Assert.IsTrue(string.IsNullOrEmpty(vm.ErrorTooltip));
}
}
[TestMethod]
public async Task AdditionalError_Success()
{
var vm = new FilePickerVM();
vm.AdditionalError = Observable.Return<IErrorResponse>(ErrorResponse.Succeed());
await Task.Delay(250);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
Assert.IsTrue(string.IsNullOrEmpty(vm.ErrorTooltip));
}
[TestMethod]
public async Task AdditionalError_Fail()
{
var vm = new FilePickerVM();
string errText = "An error";
vm.AdditionalError = Observable.Return<IErrorResponse>(ErrorResponse.Fail(errText));
await Task.Delay(250);
Assert.IsFalse(vm.ErrorState.Succeeded);
Assert.IsTrue(vm.InError);
Assert.AreEqual(errText, vm.ErrorTooltip);
}
[TestMethod]
public async Task FileExistsButSetToFolder()
{
var vm = new FilePickerVM();
using (CreateSetFile(vm))
{
vm.PathType = FilePickerVM.PathTypeOptions.Folder;
vm.ExistCheckOption = FilePickerVM.CheckOptions.On;
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsFalse(vm.ErrorState.Succeeded);
Assert.IsTrue(vm.InError);
Assert.AreEqual(FilePickerVM.PathDoesNotExistText, vm.ErrorTooltip);
}
}
[TestMethod]
public async Task FolderExistsButSetToFile()
{
var vm = new FilePickerVM();
using (CreateSetFolder(vm))
{
vm.PathType = FilePickerVM.PathTypeOptions.File;
vm.ExistCheckOption = FilePickerVM.CheckOptions.On;
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsFalse(vm.ErrorState.Succeeded);
Assert.IsTrue(vm.InError);
Assert.AreEqual(FilePickerVM.PathDoesNotExistText, vm.ErrorTooltip);
}
}
[TestMethod]
public async Task FileWithFilters_Passes()
{
var vm = new FilePickerVM();
using (CreateSetFile(vm))
{
vm.PathType = FilePickerVM.PathTypeOptions.File;
vm.ExistCheckOption = FilePickerVM.CheckOptions.Off;
vm.Filters.Add(new Microsoft.WindowsAPICodePack.Dialogs.CommonFileDialogFilter("test", $"*.{Path.GetExtension(vm.TargetPath)}"));
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
Assert.IsTrue(string.IsNullOrEmpty(vm.ErrorTooltip));
}
}
[TestMethod]
public async Task FileWithFilters_ExistsButFails()
{
var vm = new FilePickerVM();
using (CreateSetFile(vm))
{
vm.PathType = FilePickerVM.PathTypeOptions.File;
vm.ExistCheckOption = FilePickerVM.CheckOptions.Off;
vm.Filters.Add(new Microsoft.WindowsAPICodePack.Dialogs.CommonFileDialogFilter("test", $"*.{Path.GetExtension(vm.TargetPath)}z"));
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsFalse(vm.ErrorState.Succeeded);
Assert.IsTrue(vm.InError);
Assert.AreEqual(FilePickerVM.DoesNotPassFiltersText, vm.ErrorTooltip);
}
}
[TestMethod]
public async Task FileWithFilters_PassesButDoesntExist()
{
var vm = new FilePickerVM();
vm.PathType = FilePickerVM.PathTypeOptions.File;
vm.ExistCheckOption = FilePickerVM.CheckOptions.Off;
vm.TargetPath = "SomePath.png";
vm.Filters.Add(new Microsoft.WindowsAPICodePack.Dialogs.CommonFileDialogFilter("test", $"*.{Path.GetExtension(vm.TargetPath)}"));
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
Assert.IsTrue(string.IsNullOrEmpty(vm.ErrorTooltip));
}
[TestMethod]
public async Task FileWithFilters_IfNotEmptyCheck_DoesntExist()
{
var vm = new FilePickerVM();
vm.PathType = FilePickerVM.PathTypeOptions.File;
vm.ExistCheckOption = FilePickerVM.CheckOptions.Off;
vm.FilterCheckOption = FilePickerVM.CheckOptions.IfPathNotEmpty;
vm.Filters.Add(new Microsoft.WindowsAPICodePack.Dialogs.CommonFileDialogFilter("test", $"*.{Path.GetExtension(vm.TargetPath)}"));
await Task.Delay(250);
Assert.IsFalse(vm.Exists);
Assert.IsTrue(vm.ErrorState.Succeeded);
Assert.IsFalse(vm.InError);
Assert.IsTrue(string.IsNullOrEmpty(vm.ErrorTooltip));
}
}
}

View File

@ -105,6 +105,7 @@
<Compile Include="DownloaderTests.cs" />
<Compile Include="EndToEndTests.cs" />
<Compile Include="Extensions.cs" />
<Compile Include="FilePickerTests.cs" />
<Compile Include="MiscTests.cs" />
<Compile Include="ModlistMetadataTests.cs" />
<Compile Include="TestUtils.cs" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 512 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -20,10 +20,11 @@
<!-- Colors -->
<Color x:Key="WindowBackgroundColor">#121212</Color>
<Color x:Key="DarkBackgroundColor">#222222</Color>
<Color x:Key="DarkHoverBackgroundColor">#272727</Color>
<Color x:Key="LightBackgroundColor">#424242</Color>
<Color x:Key="BackgroundColor">#323232</Color>
<Color x:Key="DisabledBackgroundColor">#424242</Color>
<Color x:Key="PressedBackgroundColor">#394140</Color>
<Color x:Key="PressedBackgroundColor">#323232</Color>
<Color x:Key="LightDisabledBackgroundColor">#666666</Color>
<Color x:Key="HeatedBorderColor">#362675</Color>
@ -49,6 +50,7 @@
<Color x:Key="LightSecondary">#8cede5</Color>
<Color x:Key="IntenseSecondary">#00ffe7</Color>
<Color x:Key="Complementary">#C7FC86</Color>
<Color x:Key="DarkComplementary">#8eb55e</Color>
<Color x:Key="IntenseComplementary">#abf74d</Color>
<Color x:Key="Analogous1">#868CFC</Color>
<Color x:Key="Analogous2">#F686FC</Color>
@ -102,6 +104,7 @@
<SolidColorBrush x:Key="ErrorBrush" Color="{StaticResource Red}" />
<SolidColorBrush x:Key="DarkBackgroundBrush" Color="{StaticResource DarkBackgroundColor}" />
<SolidColorBrush x:Key="DarkHoverBackgroundBrush" Color="{StaticResource DarkHoverBackgroundColor}" />
<SolidColorBrush x:Key="LightBackgroundBrush" Color="{StaticResource LightBackgroundColor}" />
<SolidColorBrush x:Key="BackgroundBrush" Color="{StaticResource BackgroundColor}" />
<SolidColorBrush x:Key="ForegroundBrush" Color="{StaticResource ForegroundColor}" />
@ -122,6 +125,7 @@
<SolidColorBrush x:Key="LightSecondaryBrush" Color="{StaticResource LightSecondary}" />
<SolidColorBrush x:Key="IntenseSecondaryBrush" Color="{StaticResource IntenseSecondary}" />
<SolidColorBrush x:Key="ComplementaryBrush" Color="{StaticResource Complementary}" />
<SolidColorBrush x:Key="DarkComplementaryBrush" Color="{StaticResource DarkComplementary}" />
<SolidColorBrush x:Key="IntenseComplementaryBrush" Color="{StaticResource IntenseComplementary}" />
<SolidColorBrush x:Key="Analogous1Brush" Color="{StaticResource Analogous1}" />
<SolidColorBrush x:Key="Analogous2Brush" Color="{StaticResource Analogous2}" />
@ -1453,7 +1457,7 @@
<Style x:Key="MainButtonStyle" TargetType="{x:Type Button}">
<Setter Property="FocusVisualStyle" Value="{StaticResource ButtonFocusVisual}" />
<Setter Property="Background" Value="{StaticResource ButtonBackground}" />
<Setter Property="Background" Value="{StaticResource DarkBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource ButtonBorder}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Focusable" Value="False" />
@ -1466,7 +1470,7 @@
<ControlTemplate TargetType="{x:Type Button}">
<Grid>
<Border
Background="{StaticResource SecondaryBrush}"
Background="{StaticResource ComplementaryBrush}"
CornerRadius="3"
Opacity="0.7">
<Border.Effect>
@ -1498,14 +1502,14 @@
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Background" Value="{StaticResource MouseOverButtonBackground}" />
<Setter Property="BorderBrush" Value="{StaticResource DarkSecondaryBrush}" />
<Setter Property="Foreground" Value="{StaticResource MouseOverButtonForeground}" />
<Setter Property="Background" Value="{StaticResource DarkHoverBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource DarkComplementaryBrush}" />
<Setter Property="Foreground" Value="{StaticResource ComplementaryBrush}" />
</Trigger>
<Trigger Property="IsPressed" Value="true">
<Setter Property="Background" Value="{StaticResource PressedButtonBackground}" />
<Setter Property="BorderBrush" Value="{StaticResource SecondaryBrush}" />
<Setter Property="Foreground" Value="{StaticResource ButtonForeground}" />
<Setter Property="BorderBrush" Value="{StaticResource ComplementaryBrush}" />
<Setter Property="Foreground" Value="{StaticResource IntenseComplementaryBrush}" />
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="{StaticResource DisabledButtonForeground}" />
@ -1552,6 +1556,40 @@
</Style.Triggers>
</Style>
<Style x:Key="IconBareButtonStyle" TargetType="ButtonBase">
<Setter Property="Focusable" Value="False" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Background="{TemplateBinding Background}" CornerRadius="3">
<ContentPresenter
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="{StaticResource DisabledButtonForeground}" />
<Setter Property="Background" Value="Transparent" />
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="BorderBrush" Value="{StaticResource GrayBrush7}" />
<Setter Property="Foreground" Value="{StaticResource IntenseComplementaryBrush}" />
<Setter Property="Background" Value="Transparent" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="{StaticResource BackgroundBrush}" />
</Trigger>
</Style.Triggers>
</Style>
<!-- ToggleButton -->
<Style TargetType="{x:Type ToggleButton}">
<Setter Property="FocusVisualStyle" Value="{StaticResource ButtonFocusVisual}" />

View File

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Wabbajack.Common;
namespace Wabbajack
{
public class CPUDisplayVM
{
public CPUStatus Status { get; set; }
public DateTime StartTime { get; set; }
public void AbsorbStatus(CPUStatus cpu)
{
bool starting = cpu.IsWorking && ((!Status?.IsWorking) ?? true);
Status = cpu;
if (starting)
{
StartTime = DateTime.Now;
}
}
}
}

View File

@ -3,7 +3,11 @@ 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.Windows.Media.Imaging;
@ -35,26 +39,48 @@ namespace Wabbajack
private readonly ObservableAsPropertyHelper<float> _percentCompleted;
public float PercentCompleted => _percentCompleted.Value;
public ObservableCollectionExtended<CPUStatus> StatusList { get; } = new ObservableCollectionExtended<CPUStatus>();
public ObservableCollectionExtended<CPUDisplayVM> StatusList { get; } = new ObservableCollectionExtended<CPUDisplayVM>();
public ObservableCollectionExtended<IStatusMessage> Log => MWVM.Log;
public IReactiveCommand BackCommand { get; }
public IReactiveCommand GoToModlistCommand { get; }
public IReactiveCommand CloseWhenCompleteCommand { get; }
public FilePickerVM OutputLocation { get; }
private readonly ObservableAsPropertyHelper<IUserIntervention> _ActiveGlobalUserIntervention;
public IUserIntervention ActiveGlobalUserIntervention => _ActiveGlobalUserIntervention.Value;
private readonly ObservableAsPropertyHelper<bool> _Completed;
public bool Completed => _Completed.Value;
/// <summary>
/// Tracks whether compilation has begun
/// </summary>
[Reactive]
public bool CompilationMode { get; set; }
public CompilerVM(MainWindowVM mainWindowVM)
{
MWVM = mainWindowVM;
OutputLocation = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.CheckOptions.IfPathNotEmpty,
PathType = FilePickerVM.PathTypeOptions.Folder,
PromptTitle = "Select the folder to place the resulting modlist.wabbajack file",
};
// 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);
@ -108,39 +134,73 @@ namespace Wabbajack
.ToProperty(this, nameof(Compiling));
BackCommand = ReactiveCommand.Create(
execute: () => mainWindowVM.ActivePane = mainWindowVM.ModeSelectionVM,
execute: () =>
{
mainWindowVM.ActivePane = mainWindowVM.ModeSelectionVM;
CompilationMode = false;
},
canExecute: this.WhenAny(x => x.Compiling)
.Select(x => !x));
// Compile progress updates and populate ObservableCollection
Dictionary<int, CPUDisplayVM> cpuDisplays = new Dictionary<int, CPUDisplayVM>();
this.WhenAny(x => x.Compiler.ActiveCompilation)
.SelectMany(c => c?.QueueStatus ?? Observable.Empty<CPUStatus>())
.ObserveOn(RxApp.TaskpoolScheduler)
.ToObservableChangeSet(x => x.ID)
// Attach start times to incoming CPU items
.Scan(
new CPUDisplayVM(),
(_, cpu) =>
{
var ret = cpuDisplays.TryCreate(cpu.ID);
ret.AbsorbStatus(cpu);
return ret;
})
.ToObservableChangeSet(x => x.Status.ID)
.Batch(TimeSpan.FromMilliseconds(250), RxApp.TaskpoolScheduler)
.EnsureUniqueChanges()
.Filter(i => i.IsWorking)
.Filter(i => i.Status.IsWorking && i.Status.ID != WorkQueue.UnassignedCpuId)
.ObserveOn(RxApp.MainThreadScheduler)
.Sort(SortExpressionComparer<CPUStatus>.Ascending(s => s.ID), SortOptimisations.ComparesImmutableValuesOnly)
.Sort(SortExpressionComparer<CPUDisplayVM>.Ascending(s => s.StartTime))
.Bind(StatusList)
.Subscribe()
.DisposeWith(CompositeDisposable);
_Completed = Observable.CombineLatest(
this.WhenAny(x => x.Compiling),
this.WhenAny(x => x.CompilationMode),
resultSelector: (installing, installingMode) =>
{
return installingMode && !installing;
})
.ToProperty(this, nameof(Completed));
_percentCompleted = this.WhenAny(x => x.Compiler.ActiveCompilation)
.StartWith(default(ACompiler))
.Pairwise()
.Select(c =>
{
if (c.Current == null)
.CombineLatest(
this.WhenAny(x => x.Completed),
(compiler, completed) =>
{
return Observable.Return<float>(c.Previous == null ? 0f : 1f);
}
return c.Current.PercentCompleted;
})
if (compiler == null)
{
return Observable.Return<float>(completed ? 1f : 0f);
}
return compiler.PercentCompleted;
})
.Switch()
.Debounce(TimeSpan.FromMilliseconds(25))
.ToProperty(this, nameof(PercentCompleted));
// When sub compiler begins an install, mark state variable
this.WhenAny(x => x.Compiler.BeginCommand)
.Select(x => x?.StartingExecution() ?? Observable.Empty<Unit>())
.Switch()
.Subscribe(_ =>
{
CompilationMode = 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>())
@ -156,6 +216,27 @@ namespace Wabbajack
.QueryWhenChanged(query => query.FirstOrDefault())
.ObserveOnGuiThread()
.ToProperty(this, nameof(ActiveGlobalUserIntervention));
CloseWhenCompleteCommand = ReactiveCommand.Create(
canExecute: this.WhenAny(x => x.Completed),
execute: () =>
{
MWVM.ShutdownApplication();
});
GoToModlistCommand = ReactiveCommand.Create(
canExecute: this.WhenAny(x => x.Completed),
execute: () =>
{
if (string.IsNullOrWhiteSpace(OutputLocation.TargetPath))
{
Process.Start("explorer.exe", Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location));
}
else
{
Process.Start("explorer.exe", OutputLocation.TargetPath);
}
});
}
}
}

View File

@ -25,9 +25,7 @@ namespace Wabbajack
public FilePickerVM DownloadLocation { get; }
public FilePickerVM ModlistLocation { get; }
public FilePickerVM OutputLocation { get; }
public FilePickerVM ModListLocation { get; }
public IReactiveCommand BeginCommand { get; }
@ -43,26 +41,20 @@ namespace Wabbajack
public MO2CompilerVM(CompilerVM parent)
{
Parent = parent;
ModlistLocation = new FilePickerVM()
ModListLocation = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.ExistCheckOptions.On,
ExistCheckOption = FilePickerVM.CheckOptions.On,
PathType = FilePickerVM.PathTypeOptions.File,
PromptTitle = "Select modlist"
};
DownloadLocation = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.ExistCheckOptions.On,
ExistCheckOption = FilePickerVM.CheckOptions.On,
PathType = FilePickerVM.PathTypeOptions.Folder,
PromptTitle = "Select download location",
};
OutputLocation = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.ExistCheckOptions.IfNotEmpty,
PathType = FilePickerVM.PathTypeOptions.Folder,
PromptTitle = "Select the folder to place the resulting modlist.wabbajack file",
};
_mo2Folder = this.WhenAny(x => x.ModlistLocation.TargetPath)
_mo2Folder = this.WhenAny(x => x.ModListLocation.TargetPath)
.Select(loc =>
{
try
@ -76,7 +68,7 @@ namespace Wabbajack
}
})
.ToProperty(this, nameof(Mo2Folder));
_moProfile = this.WhenAny(x => x.ModlistLocation.TargetPath)
_moProfile = this.WhenAny(x => x.ModListLocation.TargetPath)
.Select(loc =>
{
try
@ -92,33 +84,64 @@ namespace Wabbajack
.ToProperty(this, nameof(MOProfile));
// Wire missing Mo2Folder to signal error state for Modlist Location
ModlistLocation.AdditionalError = this.WhenAny(x => x.Mo2Folder)
ModListLocation.AdditionalError = this.WhenAny(x => x.Mo2Folder)
.Select<string, IErrorResponse>(moFolder =>
{
if (Directory.Exists(moFolder)) 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))
.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)
// Save to property
.ObserveOnGuiThread()
.ToProperty(this, nameof(ModlistSettings));
// Wire start command
BeginCommand = ReactiveCommand.CreateFromTask(
canExecute: Observable.CombineLatest(
this.WhenAny(x => x.ModlistLocation.InError),
this.WhenAny(x => x.ModListLocation.InError),
this.WhenAny(x => x.DownloadLocation.InError),
this.WhenAny(x => x.OutputLocation.InError),
resultSelector: (ml, down, output) => !ml && !down && !output)
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)
.ObserveOnGuiThread(),
execute: async () =>
{
try
{
string outputFile;
if (string.IsNullOrWhiteSpace(OutputLocation.TargetPath))
if (string.IsNullOrWhiteSpace(parent.OutputLocation.TargetPath))
{
outputFile = MOProfile + ExtensionManager.Extension;
}
else
{
outputFile = Path.Combine(OutputLocation.TargetPath, MOProfile + ExtensionManager.Extension);
outputFile = Path.Combine(parent.OutputLocation.TargetPath, MOProfile + ExtensionManager.Extension);
}
ActiveCompilation = new MO2Compiler(
mo2Folder: Mo2Folder,
@ -160,44 +183,15 @@ namespace Wabbajack
// Load settings
_settings = parent.MWVM.Settings.Compiler.MO2Compilation;
ModlistLocation.TargetPath = _settings.LastCompiledProfileLocation;
ModListLocation.TargetPath = _settings.LastCompiledProfileLocation;
if (!string.IsNullOrWhiteSpace(_settings.DownloadLocation))
{
DownloadLocation.TargetPath = _settings.DownloadLocation;
}
OutputLocation.TargetPath = parent.MWVM.Settings.Compiler.OutputLocation;
parent.MWVM.Settings.SaveSignal
.Subscribe(_ => Unload())
.DisposeWith(CompositeDisposable);
// 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))
.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)
// Save to property
.ObserveOnGuiThread()
.ToProperty(this, nameof(ModlistSettings));
// If Mo2 folder changes and download location is empty, set it for convenience
this.WhenAny(x => x.Mo2Folder)
.DelayInitial(TimeSpan.FromMilliseconds(100))
@ -217,8 +211,7 @@ namespace Wabbajack
public void Unload()
{
_settings.DownloadLocation = DownloadLocation.TargetPath;
_settings.LastCompiledProfileLocation = ModlistLocation.TargetPath;
Parent.MWVM.Settings.Compiler.OutputLocation = OutputLocation.TargetPath;
_settings.LastCompiledProfileLocation = ModListLocation.TargetPath;
ModlistSettings?.Save();
}
}

View File

@ -1,4 +1,7 @@
using Microsoft.WindowsAPICodePack.Dialogs;
using System;
using System.Reactive.Linq;
using DynamicData;
using Microsoft.WindowsAPICodePack.Dialogs;
using ReactiveUI.Fody.Helpers;
using Wabbajack.Lib;
@ -24,27 +27,30 @@ namespace Wabbajack
[Reactive]
public string Website { get; set; }
public IObservable<bool> InError { get; }
public ModlistSettingsEditorVM(CompilationModlistSettings settings)
{
this._settings = settings;
ImagePath = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.ExistCheckOptions.IfNotEmpty,
ExistCheckOption = FilePickerVM.CheckOptions.IfPathNotEmpty,
PathType = FilePickerVM.PathTypeOptions.File,
Filters =
{
new CommonFileDialogFilter("Banner image", "*.png")
}
};
ImagePath.Filters.Add(new CommonFileDialogFilter("Banner image", "*.png"));
ReadMeText = new FilePickerVM()
{
PathType = FilePickerVM.PathTypeOptions.File,
ExistCheckOption = FilePickerVM.ExistCheckOptions.IfNotEmpty,
Filters =
{
new CommonFileDialogFilter("Text", "*.txt"),
}
ExistCheckOption = FilePickerVM.CheckOptions.IfPathNotEmpty,
};
ReadMeText.Filters.Add(new CommonFileDialogFilter("Text", "*.txt"));
InError = Observable.CombineLatest(
this.WhenAny(x => x.ImagePath.ErrorState).Select(err => err.Failed),
this.WhenAny(x => x.ReadMeText.ErrorState).Select(err => err.Failed),
resultSelector: (img, readme) => img || readme)
.Publish()
.RefCount();
}
public void Init()

View File

@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
@ -14,6 +15,8 @@ namespace Wabbajack
{
public class VortexCompilerVM : ViewModel, ISubCompilerVM
{
public CompilerVM Parent { get; }
private readonly VortexCompilationSettings _settings;
public IReactiveCommand BeginCommand { get; }
@ -53,44 +56,74 @@ namespace Wabbajack
public VortexCompilerVM(CompilerVM parent)
{
Parent = parent;
GameLocation = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.ExistCheckOptions.On,
ExistCheckOption = FilePickerVM.CheckOptions.On,
PathType = FilePickerVM.PathTypeOptions.Folder,
PromptTitle = "Select Game Folder Location"
};
DownloadsLocation = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.ExistCheckOptions.On,
ExistCheckOption = FilePickerVM.CheckOptions.On,
PathType = FilePickerVM.PathTypeOptions.Folder,
PromptTitle = "Select Downloads Folder"
};
StagingLocation = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.ExistCheckOptions.On,
ExistCheckOption = FilePickerVM.CheckOptions.On,
PathType = FilePickerVM.PathTypeOptions.Folder,
PromptTitle = "Select Staging Folder"
};
// Load custom ModList settings when game type changes
_modListSettings = this.WhenAny(x => x.SelectedGame)
.Select(game =>
{
if (game == null) return null;
var gameSettings = _settings.ModlistSettings.TryCreate(game.Game);
return new ModlistSettingsEditorVM(gameSettings.ModlistSettings);
})
// Interject and save old while loading new
.Pairwise()
.Do(pair =>
{
var (previous, current) = pair;
previous?.Save();
current?.Init();
})
.Select(x => x.Current)
// Save to property
.ObserveOnGuiThread()
.ToProperty(this, nameof(ModlistSettings));
// Wire start command
BeginCommand = ReactiveCommand.CreateFromTask(
canExecute: Observable.CombineLatest(
this.WhenAny(x => x.GameLocation.InError),
this.WhenAny(x => x.DownloadsLocation.InError),
this.WhenAny(x => x.StagingLocation.InError),
(g, d, s) => !g && !d && !s)
this.WhenAny(x => x.ModlistSettings)
.Select(x => x?.InError ?? Observable.Return(false))
.Switch(),
(g, d, s, ml) => !g && !d && !s && !ml)
.ObserveOnGuiThread(),
execute: async () =>
{
try
{
string outputFile = $"{ModlistSettings.ModListName}{ExtensionManager.Extension}";
if (!string.IsNullOrWhiteSpace(parent.OutputLocation.TargetPath))
{
outputFile = Path.Combine(parent.OutputLocation.TargetPath, outputFile);
}
ActiveCompilation = new VortexCompiler(
game: SelectedGame.Game,
gamePath: GameLocation.TargetPath,
vortexFolder: VortexCompiler.TypicalVortexFolder(),
downloadsFolder: DownloadsLocation.TargetPath,
stagingFolder: StagingLocation.TargetPath,
outputFile: $"{ModlistSettings.ModListName}{ExtensionManager.Extension}")
outputFile: outputFile)
{
ModListName = ModlistSettings.ModListName,
ModListAuthor = ModlistSettings.AuthorText,
@ -169,26 +202,6 @@ namespace Wabbajack
})
.DisposeWith(CompositeDisposable);
// Load custom ModList settings when game type changes
_modListSettings = this.WhenAny(x => x.SelectedGame)
.Select(game =>
{
var gameSettings = _settings.ModlistSettings.TryCreate(game.Game);
return new ModlistSettingsEditorVM(gameSettings.ModlistSettings);
})
// Interject and save old while loading new
.Pairwise()
.Do(pair =>
{
var (previous, current) = pair;
previous?.Save();
current?.Init();
})
.Select(x => x.Current)
// Save to property
.ObserveOnGuiThread()
.ToProperty(this, nameof(ModlistSettings));
// Find game commands
FindGameInSteamCommand = ReactiveCommand.Create(SetGameToSteamLocation);
FindGameInGogCommand = ReactiveCommand.Create(SetGameToGogLocation);

View File

@ -17,5 +17,6 @@ namespace Wabbajack
void Unload();
bool SupportsAfterInstallNavigation { get; }
void AfterInstallNavigation();
int ConfigVisualVerticalOffset { get; }
}
}

View File

@ -19,6 +19,7 @@ using DynamicData;
using DynamicData.Binding;
using Wabbajack.Common.StatusFeed;
using System.Reactive;
using System.Collections.Generic;
namespace Wabbajack
{
@ -46,7 +47,7 @@ namespace Wabbajack
public bool Installing => _installing.Value;
/// <summary>
/// Tracks whether to show the installing pane
/// Tracks whether installation has begun
/// </summary>
[Reactive]
public bool InstallingMode { get; set; }
@ -72,7 +73,7 @@ namespace Wabbajack
private readonly ObservableAsPropertyHelper<float> _percentCompleted;
public float PercentCompleted => _percentCompleted.Value;
public ObservableCollectionExtended<CPUStatus> StatusList { get; } = new ObservableCollectionExtended<CPUStatus>();
public ObservableCollectionExtended<CPUDisplayVM> StatusList { get; } = new ObservableCollectionExtended<CPUDisplayVM>();
public ObservableCollectionExtended<IStatusMessage> Log => MWVM.Log;
private readonly ObservableAsPropertyHelper<ModManager?> _TargetManager;
@ -109,7 +110,7 @@ namespace Wabbajack
ModListLocation = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.ExistCheckOptions.On,
ExistCheckOption = FilePickerVM.CheckOptions.On,
PathType = FilePickerVM.PathTypeOptions.File,
PromptTitle = "Select a modlist to install"
};
@ -179,21 +180,35 @@ namespace Wabbajack
});
BackCommand = ReactiveCommand.Create(
execute: () => mainWindowVM.ActivePane = mainWindowVM.ModeSelectionVM,
execute: () =>
{
InstallingMode = false;
mainWindowVM.ActivePane = mainWindowVM.ModeSelectionVM;
},
canExecute: this.WhenAny(x => x.Installing)
.Select(x => !x));
_Completed = Observable.CombineLatest(
this.WhenAny(x => x.Installing),
this.WhenAny(x => x.InstallingMode),
resultSelector: (installing, installingMode) =>
{
return installingMode && !installing;
})
.ToProperty(this, nameof(Completed));
_percentCompleted = this.WhenAny(x => x.Installer.ActiveInstallation)
.StartWith(default(AInstaller))
.Pairwise()
.Select(c =>
{
if (c.Current == null)
.CombineLatest(
this.WhenAny(x => x.Completed),
(installer, completed) =>
{
return Observable.Return<float>(c.Previous == null ? 0f : 1f);
}
return c.Current.PercentCompleted;
})
if (installer == null)
{
return Observable.Return<float>(completed ? 1f : 0f);
}
return installer.PercentCompleted;
})
.Switch()
.Debounce(TimeSpan.FromMilliseconds(25))
.ToProperty(this, nameof(PercentCompleted));
@ -205,8 +220,8 @@ namespace Wabbajack
_image = Observable.CombineLatest(
this.WhenAny(x => x.ModList.Error),
this.WhenAny(x => x.ModList)
.SelectMany(x => x?.ImageObservable ?? Observable.Empty<BitmapImage>())
.NotNull()
.Select(x => x?.ImageObservable ?? Observable.Empty<BitmapImage>())
.Switch()
.StartWith(WabbajackLogo),
this.WhenAny(x => x.Slideshow.Image)
.StartWith(default(BitmapImage)),
@ -217,7 +232,8 @@ namespace Wabbajack
{
return WabbajackErrLogo;
}
return installing ? slideshow : modList;
var ret = installing ? slideshow : modList;
return ret ?? WabbajackLogo;
})
.Select<BitmapImage, ImageSource>(x => x)
.ToProperty(this, nameof(Image));
@ -277,16 +293,26 @@ namespace Wabbajack
})
.ToProperty(this, nameof(ProgressTitle));
Dictionary<int, CPUDisplayVM> cpuDisplays = new Dictionary<int, CPUDisplayVM>();
// Compile progress updates and populate ObservableCollection
this.WhenAny(x => x.Installer.ActiveInstallation)
.SelectMany(c => c?.QueueStatus ?? Observable.Empty<CPUStatus>())
.ObserveOn(RxApp.TaskpoolScheduler)
.ToObservableChangeSet(x => x.ID)
// Attach start times to incoming CPU items
.Scan(
new CPUDisplayVM(),
(_, cpu) =>
{
var ret = cpuDisplays.TryCreate(cpu.ID);
ret.AbsorbStatus(cpu);
return ret;
})
.ToObservableChangeSet(x => x.Status.ID)
.Batch(TimeSpan.FromMilliseconds(250), RxApp.TaskpoolScheduler)
.EnsureUniqueChanges()
.Filter(i => i.IsWorking)
.Filter(i => i.Status.IsWorking && i.Status.ID != WorkQueue.UnassignedCpuId)
.ObserveOn(RxApp.MainThreadScheduler)
.Sort(SortExpressionComparer<CPUStatus>.Ascending(s => s.ID), SortOptimisations.ComparesImmutableValuesOnly)
.Sort(SortExpressionComparer<CPUDisplayVM>.Ascending(s => s.StartTime))
.Bind(StatusList)
.Subscribe()
.DisposeWith(CompositeDisposable);
@ -317,15 +343,6 @@ namespace Wabbajack
.ObserveOnGuiThread()
.ToProperty(this, nameof(ActiveGlobalUserIntervention));
_Completed = Observable.CombineLatest(
this.WhenAny(x => x.Installing),
this.WhenAny(x => x.InstallingMode),
resultSelector: (installing, installingMode) =>
{
return installingMode && !installing;
})
.ToProperty(this, nameof(Completed));
CloseWhenCompleteCommand = ReactiveCommand.Create(
canExecute: this.WhenAny(x => x.Completed),
execute: () =>

View File

@ -35,13 +35,15 @@ namespace Wabbajack
[Reactive]
public bool AutomaticallyOverwrite { get; set; }
public int ConfigVisualVerticalOffset => 25;
public MO2InstallerVM(InstallerVM installerVM)
{
Parent = installerVM;
Location = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.ExistCheckOptions.Off,
ExistCheckOption = FilePickerVM.CheckOptions.Off,
PathType = FilePickerVM.PathTypeOptions.Folder,
PromptTitle = "Select Installation Directory",
};
@ -49,7 +51,7 @@ namespace Wabbajack
.Select(x => Utils.IsDirectoryPathValid(x));
DownloadLocation = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.ExistCheckOptions.Off,
ExistCheckOption = FilePickerVM.CheckOptions.Off,
PathType = FilePickerVM.PathTypeOptions.Folder,
PromptTitle = "Select a location for MO2 downloads",
};

View File

@ -31,6 +31,8 @@ namespace Wabbajack
public bool SupportsAfterInstallNavigation => false;
public int ConfigVisualVerticalOffset => 0;
public VortexInstallerVM(InstallerVM installerVM)
{
Parent = installerVM;

View File

@ -3,11 +3,13 @@ using DynamicData.Binding;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using System;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using Wabbajack.Common;
using Wabbajack.Common.StatusFeed;
@ -38,6 +40,9 @@ namespace Wabbajack
public readonly UserInterventionHandlers UserInterventionHandlers;
public Dispatcher ViewDispatcher { get; set; }
public ICommand CopyVersionCommand { get; }
public string VersionDisplay { get; }
public MainWindowVM(MainWindow mainWindow, MainSettings settings)
{
MainWindow = mainWindow;
@ -79,6 +84,22 @@ namespace Wabbajack
// Start on mode selection
ActivePane = ModeSelectionVM;
}
try
{
System.Reflection.Assembly assembly = System.Reflection.Assembly.GetExecutingAssembly();
FileVersionInfo fvi = FileVersionInfo.GetVersionInfo(assembly.Location);
VersionDisplay = $"v{fvi.FileVersion}";
}
catch (Exception ex)
{
Utils.Error(ex);
VersionDisplay = "ERROR";
}
CopyVersionCommand = ReactiveCommand.Create(() =>
{
Clipboard.SetText($"Wabbajack {VersionDisplay}\n{ThisAssembly.Git.Sha}");
});
}
private static bool IsStartingFromModlist(out string modlistPath)

View File

@ -68,15 +68,10 @@ namespace Wabbajack
.ObserveOn(RxApp.MainThreadScheduler)
.Select(memStream =>
{
if (memStream == null) return default(BitmapImage);
try
{
var image = new BitmapImage();
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
image.StreamSource = memStream;
image.EndInit();
image.Freeze();
return image;
return UIUtils.BitmapImageFromStream(memStream);
}
catch (Exception ex)
{

View File

@ -69,13 +69,7 @@ namespace Wabbajack
if (memStream == null) return default(BitmapImage);
try
{
var image = new BitmapImage();
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
image.StreamSource = memStream;
image.EndInit();
image.Freeze();
return image;
return UIUtils.BitmapImageFromStream(memStream);
}
catch (Exception ex)
{

View File

@ -28,16 +28,16 @@
BorderThickness="0"
Foreground="Transparent"
Maximum="1"
Value="{Binding ProgressPercent, Mode=OneWay}" />
Value="{Binding Status.ProgressPercent, Mode=OneWay}" />
<mahapps:MetroProgressBar
Grid.Column="0"
Background="Transparent"
BorderThickness="0"
Foreground="{StaticResource PrimaryVariantBrush}"
Maximum="1"
Opacity="{Binding ProgressPercent, Mode=OneWay}"
Value="{Binding ProgressPercent, Mode=OneWay}" />
<TextBlock Grid.Column="0" Text="{Binding Msg}" />
Opacity="{Binding Status.ProgressPercent, Mode=OneWay}"
Value="{Binding Status.ProgressPercent, Mode=OneWay}" />
<TextBlock Grid.Column="0" Text="{Binding Status.Msg}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>

View File

@ -27,20 +27,20 @@
Margin="5,1,-2,1"
VerticalContentAlignment="Center"
Background="{StaticResource DarkBackgroundBrush}"
Text="{Binding TargetPath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Visibility="{Binding ShowTextBoxInput}" />
Text="{Binding PickerVM.TargetPath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Visibility="{Binding PickerVM.ShowTextBoxInput, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" />
<Grid Grid.Column="1" HorizontalAlignment="Right">
<Border
Margin="3,1,0,1"
HorizontalAlignment="Right"
Background="{StaticResource WarningBrush}"
CornerRadius="3"
ToolTip="{Binding ErrorTooltip}">
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 InError}" Value="True">
<DataTrigger Binding="{Binding PickerVM.InError, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
@ -79,7 +79,7 @@
HorizontalAlignment="Left"
Background="{StaticResource TextBoxBackground}"
CornerRadius="3">
<Button Command="{Binding SetTargetPathCommand}" ToolTip="Set target path">
<Button Command="{Binding PickerVM.SetTargetPathCommand, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" ToolTip="Set target path">
<icon:PackIconMaterial
Width="16"
Height="12"

View File

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

View File

@ -0,0 +1,139 @@
<UserControl
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"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<Border ClipToBounds="True" Style="{StaticResource AttentionBorderStyle}">
<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"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
FontFamily="Lucida Sans"
FontSize="22"
FontWeight="Black"
Text="Compilation Complete">
<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"
Width="55"
Height="55"
Command="{Binding BackCommand}"
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"
Visibility="{Binding CompilerSupportsAfterCompileNavigation, Converter={StaticResource bool2VisibilityConverter}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Button
Width="55"
Height="55"
Command="{Binding GoToModlistCommand}"
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"
Margin="0,10,0,0"
HorizontalAlignment="Center"
Text="Go To Modlist" />
</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
Width="55"
Height="55"
Command="{Binding CloseWhenCompleteCommand}"
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>
</Border>
</UserControl>

View File

@ -0,0 +1,28 @@
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 CompilationCompleteView.xaml
/// </summary>
public partial class CompilationCompleteView : UserControl
{
public CompilationCompleteView()
{
InitializeComponent();
}
}
}

View File

@ -143,7 +143,7 @@
TextWrapping="Wrap" />
<TextBlock Margin="{StaticResource TitleMargin}" Text="Image" />
<local:FilePicker
DataContext="{Binding ImagePath}"
PickerVM="{Binding ImagePath}"
Style="{StaticResource PickerStyle}"
ToolTip="Path to an image to display for the modlist." />
<TextBlock Margin="{StaticResource TitleMargin}" Text="Website" />
@ -153,7 +153,7 @@
Text="Readme Path"
ToolTip="Path to a readme file." />
<local:FilePicker
DataContext="{Binding ReadMeText}"
PickerVM="{Binding ReadMeText}"
Style="{StaticResource PickerStyle}"
ToolTip="Path to a readme file." />
</StackPanel>
@ -174,7 +174,7 @@
Margin="35,0,35,0"
VerticalAlignment="Center"
ClipToBounds="False"
Visibility="{Binding Compiling, Converter={StaticResource bool2VisibilityConverter}, ConverterParameter=False}">
Visibility="{Binding CompilationMode, Converter={StaticResource bool2VisibilityConverter}, ConverterParameter=False}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
@ -240,11 +240,11 @@
Grid.Column="0"
Grid.ColumnSpan="5"
Margin="5"
Visibility="{Binding Compiling, Converter={StaticResource bool2VisibilityConverter}, FallbackValue=Hidden}">
Visibility="{Binding CompilationMode, Converter={StaticResource bool2VisibilityConverter}, FallbackValue=Hidden}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="4*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="3*" />
</Grid.ColumnDefinitions>
<local:LogView Grid.Column="0" ProgressPercent="{Binding PercentCompleted, Mode=OneWay}" />
<local:CpuView
@ -259,6 +259,7 @@
<local:ConfirmationInterventionView DataContext="{Binding ActiveGlobalUserIntervention}" Visibility="{Binding ActiveGlobalUserIntervention, Converter={StaticResource IsTypeVisibilityConverter}, ConverterParameter={x:Type common:ConfirmationIntervention}}" />
</Grid>
</Border>
<local:CompilationCompleteView Grid.Column="2" Visibility="{Binding Completed, Converter={StaticResource bool2VisibilityConverter}, FallbackValue=Collapsed}" />
</Grid>
</Grid>
</UserControl>

View File

@ -34,7 +34,7 @@
Grid.Column="2"
Height="30"
VerticalAlignment="Center"
DataContext="{Binding ModlistLocation}"
PickerVM="{Binding ModListLocation}"
FontSize="14"
ToolTip="The MO2 modlist.txt file you want to use as your source" />
<TextBlock
@ -51,7 +51,7 @@
Grid.Column="2"
Height="30"
VerticalAlignment="Center"
DataContext="{Binding DownloadLocation}"
PickerVM="{Binding DownloadLocation}"
FontSize="14"
ToolTip="The folder where MO2 downloads your mods." />
<TextBlock
@ -68,7 +68,7 @@
Grid.Column="2"
Height="30"
VerticalAlignment="Center"
DataContext="{Binding OutputLocation}"
PickerVM="{Binding Parent.OutputLocation}"
FontSize="14"
ToolTip="The folder to place the resulting modlist.wabbajack file" />
</Grid>

View File

@ -20,13 +20,12 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="28" />
<RowDefinition Height="40" />
<RowDefinition Height="40" />
<RowDefinition Height="28" />
<RowDefinition Height="40" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="1"
Grid.Row="0"
Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
@ -35,7 +34,7 @@
TextAlignment="Center"
ToolTip="The game you wish to target" />
<ComboBox
Grid.Row="1"
Grid.Row="0"
Grid.Column="2"
Height="30"
VerticalAlignment="Center"
@ -51,7 +50,7 @@
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock
Grid.Row="2"
Grid.Row="1"
Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
@ -60,15 +59,15 @@
TextAlignment="Center"
ToolTip="The install folder for the game" />
<local:FilePicker
Grid.Row="2"
Grid.Row="1"
Grid.Column="2"
Height="30"
VerticalAlignment="Center"
DataContext="{Binding GameLocation}"
PickerVM="{Binding GameLocation}"
FontSize="14"
ToolTip="The install folder for the game" />
<Grid
Grid.Row="3"
Grid.Row="2"
Grid.Column="2"
Height="28"
HorizontalAlignment="Left"
@ -97,36 +96,53 @@
</Grid>
<TextBlock
Grid.Row="1"
Grid.Row="0"
Grid.Column="4"
HorizontalAlignment="Right"
VerticalAlignment="Center"
FontSize="14"
Text="Download Location"
TextAlignment="Center"
ToolTip="The folder to downloads your mods" />
ToolTip="The folder to download your mods" />
<local:FilePicker
Grid.Row="1"
Grid.Row="0"
Grid.Column="6"
Height="30"
VerticalAlignment="Center"
DataContext="{Binding DownloadsLocation}"
PickerVM="{Binding DownloadsLocation}"
FontSize="14"
ToolTip="The folder to downloads your mods" />
ToolTip="The folder to download your mods" />
<TextBlock
Grid.Row="2"
Grid.Row="1"
Grid.Column="4"
HorizontalAlignment="Right"
VerticalAlignment="Center"
FontSize="14"
Text="Staging Location"
TextAlignment="Center" />
<local:FilePicker
Grid.Row="1"
Grid.Column="6"
Height="30"
VerticalAlignment="Center"
PickerVM="{Binding StagingLocation}"
FontSize="14" />
<TextBlock
Grid.Row="2"
Grid.Column="4"
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="6"
Height="30"
VerticalAlignment="Center"
DataContext="{Binding StagingLocation}"
FontSize="14" />
PickerVM="{Binding Parent.OutputLocation}"
FontSize="14"
ToolTip="The folder to place the resulting modlist.wabbajack file" />
</Grid>
</UserControl>

View File

@ -9,7 +9,7 @@
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<Border Style="{StaticResource AttentionBorderStyle}" ClipToBounds="True">
<Border ClipToBounds="True" Style="{StaticResource AttentionBorderStyle}">
<Grid Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
@ -19,11 +19,12 @@
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="3"
Grid.ColumnSpan="4"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
FontFamily="Lucida Sans"
@ -44,13 +45,13 @@
</Grid.RowDefinitions>
<Button
Grid.Row="0"
Width="55"
Height="55"
Width="50"
Height="50"
Command="{Binding BackCommand}"
Style="{StaticResource CircleButtonStyle}">
<icon:PackIconMaterial
Width="28"
Height="28"
Width="25"
Height="25"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
Kind="ArrowLeft" />
</Button>
@ -70,13 +71,13 @@
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Button
Width="55"
Height="55"
Width="50"
Height="50"
Command="{Binding GoToInstallCommand}"
Style="{StaticResource CircleButtonStyle}">
<icon:PackIconMaterial
Width="25"
Height="25"
Width="23"
Height="23"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
Kind="FolderMove" />
</Button>
@ -84,7 +85,7 @@
Grid.Row="1"
Margin="0,10,0,0"
HorizontalAlignment="Center"
Text="Open Install Folder" />
Text="Install Folder" />
</Grid>
<Grid
Grid.Row="1"
@ -95,36 +96,40 @@
<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
Width="55"
Height="55"
Width="50"
Height="50"
Command="{Binding OpenReadmeCommand}"
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="3"
VerticalAlignment="Center"
Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Button
Width="50"
Height="50"
Command="{Binding CloseWhenCompleteCommand}"
Style="{StaticResource CircleButtonStyle}">
<icon:PackIconMaterial
Width="30"
Height="30"
Width="25"
Height="25"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
Kind="Check" />
</Button>

View File

@ -256,23 +256,74 @@
Grid.Row="0"
Margin="30,5"
Command="{Binding OpenReadmeCommand}"
Content="Readme"
FontSize="20"
ToolTip="Open the readme for the modlist" />
ToolTip="Open the readme for the modlist">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30" />
<ColumnDefinition Width="82" />
</Grid.ColumnDefinitions>
<icon:PackIconFontAwesome
Grid.Column="0"
Width="30"
Height="30"
VerticalAlignment="Center"
Kind="ReadmeBrands" />
<TextBlock
Grid.Column="1"
Margin="10,0,0,0"
VerticalAlignment="Center"
Text="Readme" />
</Grid>
</Button>
<Button
Grid.Row="1"
Margin="30,5"
Command="{Binding VisitWebsiteCommand}"
Content="Website"
FontSize="20"
ToolTip="Open the webpage for the modlist" />
ToolTip="Open the webpage for the modlist">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30" />
<ColumnDefinition Width="82" />
</Grid.ColumnDefinitions>
<icon:PackIconMaterial
Grid.Column="0"
Width="30"
Height="30"
VerticalAlignment="Center"
Kind="Web" />
<TextBlock
Grid.Column="1"
Margin="10,0,0,0"
VerticalAlignment="Center"
Text="Website" />
</Grid>
</Button>
<Button
Grid.Row="2"
Margin="30,5"
Command="{Binding ShowReportCommand}"
Content="Manifest"
FontSize="20"
ToolTip="Open an explicit listing of all actions this modlist will take" />
ToolTip="Open an explicit listing of all actions this modlist will take">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30" />
<ColumnDefinition Width="82" />
</Grid.ColumnDefinitions>
<icon:PackIconOcticons
Grid.Column="0"
Width="30"
Height="30"
VerticalAlignment="Center"
Kind="Checklist" />
<TextBlock
Grid.Column="1"
Margin="10,0,0,0"
VerticalAlignment="Center"
Text="Manifest" />
</Grid>
</Button>
</Grid>
<Grid
x:Name="InstallationConfigurationView"
@ -283,15 +334,44 @@
<ColumnDefinition Width="20" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ScrollViewer
<Grid
Grid.Column="0"
Margin="5"
VerticalAlignment="Center"
Background="Transparent"
VerticalScrollBarVisibility="Auto"
Visibility="{Binding InstallingMode, Converter={StaticResource bool2VisibilityConverter}, ConverterParameter=False}">
<ContentPresenter
Margin="0"
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="5" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="120" />
<ColumnDefinition Width="20" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Rectangle Grid.Row="0" Height="{Binding Installer.ConfigVisualVerticalOffset, Mode=OneWay}" />
<TextBlock
Grid.Row="1"
Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
FontSize="14"
Text="Target Modlist"
TextAlignment="Center" />
<local:FilePicker
Grid.Row="1"
Grid.Column="2"
Height="30"
VerticalAlignment="Center"
FontSize="14"
PickerVM="{Binding ModListLocation}" />
<ContentPresenter
Grid.Row="3"
Grid.Column="0"
Grid.ColumnSpan="3"
VerticalAlignment="Top"
Content="{Binding Installer}">
<ContentPresenter.Resources>
<DataTemplate DataType="{x:Type local:MO2InstallerVM}">
@ -302,7 +382,7 @@
</DataTemplate>
</ContentPresenter.Resources>
</ContentPresenter>
</ScrollViewer>
</Grid>
<local:BeginButton
Grid.Column="2"
Margin="0,0,25,0"
@ -316,9 +396,9 @@
Margin="5,0,5,5"
Visibility="{Binding InstallingMode, Converter={StaticResource bool2VisibilityConverter}, FallbackValue=Hidden}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="4*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="3*" />
</Grid.ColumnDefinitions>
<local:LogView Grid.Column="0" ProgressPercent="{Binding PercentCompleted, Mode=OneWay}" />
<local:CpuView

View File

@ -16,29 +16,12 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="20" />
<RowDefinition Height="40" />
<RowDefinition Height="40" />
<RowDefinition Height="40" />
<RowDefinition Height="20" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="1"
Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
FontSize="14"
Text="Target Modlist"
TextAlignment="Center" />
<local:FilePicker
Grid.Row="1"
Grid.Column="2"
Height="30"
VerticalAlignment="Center"
DataContext="{Binding Parent.ModListLocation}"
FontSize="14" />
<TextBlock
Grid.Row="2"
Grid.Row="0"
Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
@ -46,14 +29,14 @@
Text="Installation Location"
TextAlignment="Center" />
<local:FilePicker
Grid.Row="2"
Grid.Row="0"
Grid.Column="2"
Height="30"
VerticalAlignment="Center"
DataContext="{Binding Location}"
FontSize="14" />
FontSize="14"
PickerVM="{Binding Location}" />
<TextBlock
Grid.Row="3"
Grid.Row="1"
Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
@ -61,14 +44,14 @@
Text="Download Location"
TextAlignment="Center" />
<local:FilePicker
Grid.Row="3"
Grid.Row="1"
Grid.Column="2"
Height="30"
VerticalAlignment="Center"
DataContext="{Binding DownloadLocation}"
FontSize="14" />
FontSize="14"
PickerVM="{Binding DownloadLocation}" />
<CheckBox
Grid.Row="4"
Grid.Row="2"
Grid.Column="2"
HorizontalAlignment="Right"
Content="Overwrite Installation"

View File

@ -9,28 +9,5 @@
d:DesignWidth="800"
mc:Ignorable="d">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="120" />
<ColumnDefinition Width="20" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="40" />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
FontSize="14"
Text="Target Modlist"
TextAlignment="Center" />
<local:FilePicker
Grid.Row="0"
Grid.Column="2"
Height="30"
VerticalAlignment="Center"
DataContext="{Binding Parent.ModListLocation}"
FontSize="14" />
</Grid>
</UserControl>

View File

@ -3,6 +3,7 @@
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">
@ -12,23 +13,39 @@
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.Resources>
<Style TargetType="Image">
<Setter Property="Margin" Value="5" />
</Style>
</Grid.Resources>
<Image
Name="GitHub"
<Button
Grid.Column="0"
MouseLeftButtonDown="GitHub_MouseLeftButtonDown" Source="../Resources/Icons/github.png"/>
<Image
Name="Patreon"
Width="35"
Height="35"
Click="GitHub_Click"
Style="{StaticResource IconBareButtonStyle}">
<icon:PackIconMaterial
Width="25"
Height="25"
Kind="GithubCircle" />
</Button>
<Button
Grid.Column="1"
MouseLeftButtonDown="Patreon_MouseLeftButtonDown" Source="../Resources/Icons/patreon.png"/>
<Image
Name="Discord"
Width="35"
Height="35"
Margin="4,0,0,0"
Click="Patreon_Click"
Style="{StaticResource IconBareButtonStyle}">
<icon:PackIconMaterial
Width="25"
Height="25"
Kind="Patreon" />
</Button>
<Button
Grid.Column="2"
MouseLeftButtonDown="Discord_MouseLeftButtonDown"
Source="../Resources/Icons/discord.png" />
Width="35"
Height="35"
Click="Discord_Click"
Style="{StaticResource IconBareButtonStyle}">
<icon:PackIconMaterial
Width="25"
Height="25"
Kind="Discord" />
</Button>
</Grid>
</UserControl>

View File

@ -26,19 +26,19 @@ namespace Wabbajack
InitializeComponent();
}
private void GitHub_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
private void GitHub_Click(object sender, RoutedEventArgs e)
{
Process.Start("https://github.com/wabbajack-tools/wabbajack");
}
private void Patreon_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
private void Discord_Click(object sender, RoutedEventArgs e)
{
Process.Start("https://discord.gg/wabbajack");
}
private void Patreon_Click(object sender, RoutedEventArgs e)
{
Process.Start("https://www.patreon.com/user?u=11907933");
}
private void Discord_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Process.Start("https://discord.gg/zgbrkmA");
}
}
}

View File

@ -38,4 +38,16 @@
</DataTemplate>
</ContentPresenter.Resources>
</ContentPresenter>
<mahapps:MetroWindow.RightWindowCommands>
<mahapps:WindowCommands>
<Button Command="{Binding CopyVersionCommand}" Content="{Binding VersionDisplay}">
<Button.ToolTip>
<TextBlock>
Wabbajack Version<LineBreak />
Click to copy to clipboard</TextBlock>
</Button.ToolTip>
</Button>
</mahapps:WindowCommands>
</mahapps:MetroWindow.RightWindowCommands>
</mahapps:MetroWindow>

View File

@ -222,24 +222,6 @@
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.Resources>
<Style
x:Key="ModlistButtonStyle"
BasedOn="{StaticResource IconCircleButtonStyle}"
TargetType="Button">
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsMouseOver, ElementName=ModListTile}" Value="True" />
<Condition Binding="{Binding IsEnabled, RelativeSource={RelativeSource Self}}" Value="True" />
</MultiDataTrigger.Conditions>
<MultiDataTrigger.Setters>
<Setter Property="Foreground" Value="{StaticResource IntenseComplementaryBrush}" />
</MultiDataTrigger.Setters>
</MultiDataTrigger>
</Style.Triggers>
</Style>
</Grid.Resources>
<Button
Grid.Row="0"
Width="40"
@ -247,7 +229,7 @@
Margin="5,0"
VerticalAlignment="Bottom"
Command="{Binding OpenWebsiteCommand}"
Style="{StaticResource ModlistButtonStyle}">
Style="{StaticResource IconBareButtonStyle}">
<iconPacks:Material
Width="20"
Height="20"
@ -262,7 +244,7 @@
VerticalAlignment="Top"
Command="{Binding ExecuteCommand}">
<Button.Style>
<Style BasedOn="{StaticResource ModlistButtonStyle}" TargetType="Button">
<Style BasedOn="{StaticResource IconBareButtonStyle}" TargetType="Button">
<Setter Property="Content">
<Setter.Value>
<iconPacks:Material

View File

@ -505,5 +505,12 @@
</Grid>
</Button>
</Grid>
<local:LinksView
Grid.Row="0"
Grid.RowSpan="3"
Grid.Column="0"
Margin="10"
HorizontalAlignment="Right"
VerticalAlignment="Top" />
</Grid>
</UserControl>

View File

@ -173,6 +173,10 @@
<SubType>Designer</SubType>
</ApplicationDefinition>
<Compile Include="Converters\IsTypeVisibilityConverter.cs" />
<Compile Include="View Models\CPUDisplayVM.cs" />
<Compile Include="Views\Compilers\CompilationCompleteView.xaml.cs">
<DependentUpon>CompilationCompleteView.xaml</DependentUpon>
</Compile>
<Compile Include="Views\Installers\InstallationCompleteView.xaml.cs">
<DependentUpon>InstallationCompleteView.xaml</DependentUpon>
</Compile>
@ -228,19 +232,16 @@
<Compile Include="Views\Common\DetailImageView.xaml.cs">
<DependentUpon>DetailImageView.xaml</DependentUpon>
</Compile>
<Compile Include="Extensions\EnumerableExt.cs" />
<Compile Include="Views\Common\TopProgressView.xaml.cs">
<DependentUpon>TopProgressView.xaml</DependentUpon>
</Compile>
<Compile Include="Settings.cs" />
<Compile Include="View Models\FilePickerVM.cs" />
<Compile Include="View Models\ModListVM.cs" />
<Compile Include="View Models\ModVM.cs" />
<Compile Include="Views\Compilers\CompilerView.xaml.cs">
<DependentUpon>CompilerView.xaml</DependentUpon>
</Compile>
<Compile Include="Converters\IsNotNullVisibilityConverter.cs" />
<Compile Include="Extensions\ReactiveUIExt.cs" />
<Compile Include="View Models\ModeSelectionVM.cs" />
<Compile Include="Views\Common\FilePicker.xaml.cs">
<DependentUpon>FilePicker.xaml</DependentUpon>
@ -269,6 +270,10 @@
<Compile Include="Views\WebBrowserView.xaml.cs">
<DependentUpon>WebBrowserView.xaml</DependentUpon>
</Compile>
<Page Include="Views\Compilers\CompilationCompleteView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\Installers\InstallationCompleteView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
@ -437,12 +442,6 @@
<ItemGroup>
<EmbeddedResource Include="Resources\banner_small.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Icons\discord.png" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\Icons\next.png" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\Banner_Dark.png" />
</ItemGroup>
@ -541,10 +540,6 @@
<Resource Include="Resources\Icons\gog.png" />
<Resource Include="Resources\Icons\steam.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Icons\github.png" />
<Resource Include="Resources\Icons\patreon.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Wabba_Mouth_No_Text.png" />
</ItemGroup>