diff --git a/Wabbajack/Extensions/EnumerableExt.cs b/Wabbajack.Common/Extensions/EnumerableExt.cs similarity index 100% rename from Wabbajack/Extensions/EnumerableExt.cs rename to Wabbajack.Common/Extensions/EnumerableExt.cs diff --git a/Wabbajack.Common/Util/TempFile.cs b/Wabbajack.Common/Util/TempFile.cs new file mode 100644 index 00000000..195dc343 --- /dev/null +++ b/Wabbajack.Common/Util/TempFile.cs @@ -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(); + } + } + } +} diff --git a/Wabbajack.Common/Util/TempFolder.cs b/Wabbajack.Common/Util/TempFolder.cs new file mode 100644 index 00000000..346ea344 --- /dev/null +++ b/Wabbajack.Common/Util/TempFolder.cs @@ -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); + } + } + } +} diff --git a/Wabbajack.Common/Wabbajack.Common.csproj b/Wabbajack.Common/Wabbajack.Common.csproj index bfde1f2b..2efda655 100644 --- a/Wabbajack.Common/Wabbajack.Common.csproj +++ b/Wabbajack.Common/Wabbajack.Common.csproj @@ -75,9 +75,6 @@ MinimumRecommendedRules.ruleset - - ..\..\..\Users\tbald\.nuget\packages\syroot.windows.io.knownfolders\1.2.1\lib\net452\Syroot.KnownFolders.dll - @@ -103,6 +100,7 @@ + @@ -130,6 +128,8 @@ + + diff --git a/Wabbajack/Extensions/ReactiveUIExt.cs b/Wabbajack.Lib/Extensions/ReactiveUIExt.cs similarity index 100% rename from Wabbajack/Extensions/ReactiveUIExt.cs rename to Wabbajack.Lib/Extensions/ReactiveUIExt.cs diff --git a/Wabbajack.Lib/FodyWeavers.xml b/Wabbajack.Lib/FodyWeavers.xml new file mode 100644 index 00000000..63fc1484 --- /dev/null +++ b/Wabbajack.Lib/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/Wabbajack.Lib/FodyWeavers.xsd b/Wabbajack.Lib/FodyWeavers.xsd new file mode 100644 index 00000000..f3ac4762 --- /dev/null +++ b/Wabbajack.Lib/FodyWeavers.xsd @@ -0,0 +1,26 @@ + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/Wabbajack/View Models/FilePickerVM.cs b/Wabbajack.Lib/UI/FilePickerVM.cs similarity index 57% rename from Wabbajack/View Models/FilePickerVM.cs rename to Wabbajack.Lib/UI/FilePickerVM.cs index 33d010ee..dc6146de 100644 --- a/Wabbajack/View Models/FilePickerVM.cs +++ b/Wabbajack.Lib/UI/FilePickerVM.cs @@ -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 AdditionalError { get; set; } @@ -60,46 +64,49 @@ namespace Wabbajack private readonly ObservableAsPropertyHelper _errorTooltip; public string ErrorTooltip => _errorTooltip.Value; - public List Filters { get; } = new List(); + public SourceList Filters { get; } = new SourceList(); 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 +120,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 +137,82 @@ 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($"Path does not pass designated filters"); + }) + .Publish() + .RefCount(); _errorState = Observable.CombineLatest( - this.WhenAny(x => x.Exists) + this.WhenAny(x => x.Exists), + Observable.CombineLatest( + this.WhenAny(x => x.Exists), + doExistsCheck, + resultSelector: (exists, doExists) => !doExists || exists) .Select(exists => ErrorResponse.Create(successful: exists, exists ? default(string) : "Path does not exist")), + passesFilters, this.WhenAny(x => x.AdditionalError) .Select(x => x ?? Observable.Return(ErrorResponse.Success)) .Switch(), - resultSelector: (exist, err) => + resultSelector: (exists, 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)); @@ -161,12 +226,15 @@ namespace Wabbajack _errorTooltip = Observable.CombineLatest( this.WhenAny(x => x.Exists) .Select(exists => exists ? default(string) : "Path does not exist"), + passesFilters + .Select(x => x.Reason), this.WhenAny(x => x.AdditionalError) .Select(x => x ?? Observable.Return(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 +269,7 @@ namespace Wabbajack Multiselect = false, ShowPlacesList = true, }; - foreach (var filter in Filters) + foreach (var filter in Filters.Items) { dlg.Filters.Add(filter); } diff --git a/Wabbajack.Lib/Wabbajack.Lib.csproj b/Wabbajack.Lib/Wabbajack.Lib.csproj index faa70202..512c3af5 100644 --- a/Wabbajack.Lib/Wabbajack.Lib.csproj +++ b/Wabbajack.Lib/Wabbajack.Lib.csproj @@ -59,7 +59,6 @@ - @@ -119,6 +118,7 @@ + @@ -145,6 +145,7 @@ + @@ -210,6 +211,9 @@ 11.0.1 + + 11.0.1 + 0.24.0 diff --git a/Wabbajack.Test/FilePickerTests.cs b/Wabbajack.Test/FilePickerTests.cs new file mode 100644 index 00000000..61f8f748 --- /dev/null +++ b/Wabbajack.Test/FilePickerTests.cs @@ -0,0 +1,344 @@ +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); + } + + [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); + } + + [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); + } + } + + [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); + } + + [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); + } + } + + [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); + } + + [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); + } + + [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); + } + } + + [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); + } + + [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); + } + } + + [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); + } + + [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); + } + + [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); + } + } + + [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); + } + + [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); + } + } + + [TestMethod] + public async Task AdditionalError_Success() + { + var vm = new FilePickerVM(); + vm.AdditionalError = Observable.Return(ErrorResponse.Succeed()); + await Task.Delay(250); + Assert.IsTrue(vm.ErrorState.Succeeded); + Assert.IsFalse(vm.InError); + } + + [TestMethod] + public async Task AdditionalError_Fail() + { + var vm = new FilePickerVM(); + vm.AdditionalError = Observable.Return(ErrorResponse.Fail()); + await Task.Delay(250); + Assert.IsFalse(vm.ErrorState.Succeeded); + Assert.IsTrue(vm.InError); + } + + [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); + } + } + + [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); + } + } + + [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); + } + } + + [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); + } + } + + [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); + } + + [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); + } + } +} diff --git a/Wabbajack.Test/Wabbajack.Test.csproj b/Wabbajack.Test/Wabbajack.Test.csproj index b44c382b..626500ab 100644 --- a/Wabbajack.Test/Wabbajack.Test.csproj +++ b/Wabbajack.Test/Wabbajack.Test.csproj @@ -105,6 +105,7 @@ + diff --git a/Wabbajack/View Models/Compilers/CompilerVM.cs b/Wabbajack/View Models/Compilers/CompilerVM.cs index 58397d41..80f89219 100644 --- a/Wabbajack/View Models/Compilers/CompilerVM.cs +++ b/Wabbajack/View Models/Compilers/CompilerVM.cs @@ -66,7 +66,7 @@ namespace Wabbajack OutputLocation = new FilePickerVM() { - ExistCheckOption = FilePickerVM.ExistCheckOptions.IfNotEmpty, + ExistCheckOption = FilePickerVM.CheckOptions.IfPathNotEmpty, PathType = FilePickerVM.PathTypeOptions.Folder, PromptTitle = "Select the folder to place the resulting modlist.wabbajack file", }; diff --git a/Wabbajack/View Models/Compilers/MO2CompilerVM.cs b/Wabbajack/View Models/Compilers/MO2CompilerVM.cs index 28bb50f7..af77bd93 100644 --- a/Wabbajack/View Models/Compilers/MO2CompilerVM.cs +++ b/Wabbajack/View Models/Compilers/MO2CompilerVM.cs @@ -43,13 +43,13 @@ namespace Wabbajack Parent = parent; 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", }; @@ -91,13 +91,44 @@ namespace Wabbajack 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.DownloadLocation.InError), parent.WhenAny(x => x.OutputLocation.InError), - resultSelector: (ml, down, output) => !ml && !down && !output) + 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 () => { @@ -161,34 +192,6 @@ namespace Wabbajack .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)) diff --git a/Wabbajack/View Models/Compilers/ModlistSettingsEditorVM.cs b/Wabbajack/View Models/Compilers/ModlistSettingsEditorVM.cs index b7cc89bc..426f7c07 100644 --- a/Wabbajack/View Models/Compilers/ModlistSettingsEditorVM.cs +++ b/Wabbajack/View Models/Compilers/ModlistSettingsEditorVM.cs @@ -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 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() diff --git a/Wabbajack/View Models/Compilers/VortexCompilerVM.cs b/Wabbajack/View Models/Compilers/VortexCompilerVM.cs index 4fba5251..fde9626e 100644 --- a/Wabbajack/View Models/Compilers/VortexCompilerVM.cs +++ b/Wabbajack/View Models/Compilers/VortexCompilerVM.cs @@ -59,30 +59,53 @@ namespace Wabbajack 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 => + { + 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 () => { @@ -178,26 +201,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); diff --git a/Wabbajack/View Models/Installers/InstallerVM.cs b/Wabbajack/View Models/Installers/InstallerVM.cs index 18a82818..c3e4361d 100644 --- a/Wabbajack/View Models/Installers/InstallerVM.cs +++ b/Wabbajack/View Models/Installers/InstallerVM.cs @@ -109,7 +109,7 @@ namespace Wabbajack ModListLocation = new FilePickerVM() { - ExistCheckOption = FilePickerVM.ExistCheckOptions.On, + ExistCheckOption = FilePickerVM.CheckOptions.On, PathType = FilePickerVM.PathTypeOptions.File, PromptTitle = "Select a modlist to install" }; diff --git a/Wabbajack/View Models/Installers/MO2InstallerVM.cs b/Wabbajack/View Models/Installers/MO2InstallerVM.cs index a35af134..d3ba8292 100644 --- a/Wabbajack/View Models/Installers/MO2InstallerVM.cs +++ b/Wabbajack/View Models/Installers/MO2InstallerVM.cs @@ -41,7 +41,7 @@ namespace Wabbajack Location = new FilePickerVM() { - ExistCheckOption = FilePickerVM.ExistCheckOptions.Off, + ExistCheckOption = FilePickerVM.CheckOptions.Off, PathType = FilePickerVM.PathTypeOptions.Folder, PromptTitle = "Select Installation Directory", }; @@ -49,7 +49,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", }; diff --git a/Wabbajack/Wabbajack.csproj b/Wabbajack/Wabbajack.csproj index 7b95922a..6d3b316b 100644 --- a/Wabbajack/Wabbajack.csproj +++ b/Wabbajack/Wabbajack.csproj @@ -231,19 +231,16 @@ DetailImageView.xaml - TopProgressView.xaml - CompilerView.xaml - FilePicker.xaml