Merge branch 'master' into login-manager

This commit is contained in:
Timothy Baldridge 2019-12-20 16:17:08 -07:00 committed by GitHub
commit 698a419973
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1081 additions and 714 deletions

View File

@ -144,7 +144,7 @@ namespace Wabbajack.CacheServer
{
foreach (var list in modlists)
{
var modlist_path = Path.Combine(Consts.ModListDownloadFolder, list.Links.MachineURL + ".wabbajack");
var modlist_path = Path.Combine(Consts.ModListDownloadFolder, list.Links.MachineURL + ExtensionManager.Extension);
if (list.NeedsDownload(modlist_path))
{

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Data.HashFunction.xxHash;
using System.Diagnostics;
@ -329,7 +329,7 @@ namespace Wabbajack.Common
return new DynamicIniData(new FileIniDataParser().ReadData(new StreamReader(new MemoryStream(Encoding.UTF8.GetBytes(file)))));
}
public static void ToCERAS<T>(this T obj, string filename, ref SerializerConfig config)
public static void ToCERAS<T>(this T obj, string filename, SerializerConfig config)
{
var ceras = new CerasSerializer(config);
byte[] buffer = null;
@ -343,7 +343,7 @@ namespace Wabbajack.Common
}
}
public static T FromCERAS<T>(this Stream data, ref SerializerConfig config)
public static T FromCERAS<T>(this Stream data, SerializerConfig config)
{
var ceras = new CerasSerializer(config);
byte[] bytes = data.ReadAll();
@ -630,6 +630,44 @@ namespace Wabbajack.Common
return await Task.WhenAll(tasks);
}
public static async Task PMap<TI>(this IEnumerable<TI> coll, WorkQueue queue,
Func<TI, Task> f)
{
var colllst = coll.ToList();
var remainingTasks = colllst.Count;
var tasks = colllst.Select(i =>
{
var tc = new TaskCompletionSource<bool>();
queue.QueueTask(async () =>
{
try
{
await f(i);
tc.SetResult(true);
}
catch (Exception ex)
{
tc.SetException(ex);
}
Interlocked.Decrement(ref remainingTasks);
});
return tc.Task;
}).ToList();
// To avoid thread starvation, we'll start to help out in the work queue
if (WorkQueue.WorkerThread)
while (remainingTasks > 0)
if (queue.Queue.TryTake(out var a, 500))
{
WorkQueue.AsyncLocalCurrentQueue.Value = WorkQueue.ThreadLocalCurrentQueue.Value;
await a();
}
await Task.WhenAll(tasks);
}
public static async Task PMap<TI>(this IEnumerable<TI> coll, WorkQueue queue, Action<TI> f)
{
await coll.PMap(queue, i =>

View File

@ -22,6 +22,7 @@ namespace Wabbajack.Lib
public abstract class ACompiler : ABatchProcessor
{
public string ModListName, ModListAuthor, ModListDescription, ModListImage, ModListWebsite, ModListReadme;
public bool ReadmeIsWebsite;
public string WabbajackVersion;
protected static string _vfsCacheName = "vfs_compile_cache.bin";
@ -94,8 +95,10 @@ namespace Wabbajack.Lib
ModList.Readme = $"readme{readme.Extension}";
}
ModList.ReadmeIsWebsite = ReadmeIsWebsite;
//ModList.ToJSON(Path.Combine(ModListOutputFolder, "modlist.json"));
ModList.ToCERAS(Path.Combine(ModListOutputFolder, "modlist"), ref CerasConfig.Config);
ModList.ToCERAS(Path.Combine(ModListOutputFolder, "modlist"), CerasConfig.Config);
if (File.Exists(ModListOutputFile))
File.Delete(ModListOutputFile);

View File

@ -78,7 +78,7 @@ namespace Wabbajack.Lib
return e.FromJSON<ModList>();
}
using (var e = entry.Open())
return e.FromCERAS<ModList>(ref CerasConfig.Config);
return e.FromCERAS<ModList>(CerasConfig.Config);
}
}

View File

@ -8,25 +8,31 @@ namespace Wabbajack.Lib
{
public class CerasConfig
{
public static SerializerConfig Config = new SerializerConfig
{
KnownTypes =
{
typeof(ModList), typeof(Game), typeof(Directive), typeof(IgnoredDirectly),
typeof(NoMatch), typeof(InlineFile), typeof(PropertyType), typeof(CleanedESM),
typeof(RemappedInlineFile), typeof(FromArchive), typeof(CreateBSA), typeof(PatchedFromArchive),
typeof(SourcePatch), typeof(MergedPatch), typeof(Archive), typeof(IndexedArchive), typeof(IndexedEntry),
typeof(IndexedArchiveEntry), typeof(BSAIndexedEntry), typeof(VirtualFile),
typeof(ArchiveStateObject), typeof(FileStateObject), typeof(IDownloader),
typeof(IUrlDownloader), typeof(AbstractDownloadState), typeof(ManualDownloader.State),
typeof(DropboxDownloader), typeof(GoogleDriveDownloader.State), typeof(HTTPDownloader.State),
typeof(MegaDownloader.State), typeof(ModDBDownloader.State), typeof(NexusDownloader.State),
typeof(BSAStateObject), typeof(BSAFileStateObject), typeof(BA2StateObject), typeof(BA2DX10EntryState),
typeof(BA2FileEntryState), typeof(MediaFireDownloader.State), typeof(ArchiveMeta),
typeof(PropertyFile), typeof(SteamMeta), typeof(SteamWorkshopDownloader), typeof(SteamWorkshopDownloader.State),
typeof(LoversLabDownloader.State), typeof(GameFileSourceDownloader.State)
public static readonly SerializerConfig Config;
}
};
static CerasConfig()
{
Config = new SerializerConfig
{
KnownTypes =
{
typeof(ModList), typeof(Game), typeof(Directive), typeof(IgnoredDirectly),
typeof(NoMatch), typeof(InlineFile), typeof(PropertyType), typeof(CleanedESM),
typeof(RemappedInlineFile), typeof(FromArchive), typeof(CreateBSA), typeof(PatchedFromArchive),
typeof(SourcePatch), typeof(MergedPatch), typeof(Archive), typeof(IndexedArchive), typeof(IndexedEntry),
typeof(IndexedArchiveEntry), typeof(BSAIndexedEntry), typeof(VirtualFile),
typeof(ArchiveStateObject), typeof(FileStateObject), typeof(IDownloader),
typeof(IUrlDownloader), typeof(AbstractDownloadState), typeof(ManualDownloader.State),
typeof(DropboxDownloader), typeof(GoogleDriveDownloader.State), typeof(HTTPDownloader.State),
typeof(MegaDownloader.State), typeof(ModDBDownloader.State), typeof(NexusDownloader.State),
typeof(BSAStateObject), typeof(BSAFileStateObject), typeof(BA2StateObject), typeof(BA2DX10EntryState),
typeof(BA2FileEntryState), typeof(MediaFireDownloader.State), typeof(ArchiveMeta),
typeof(PropertyFile), typeof(SteamMeta), typeof(SteamWorkshopDownloader), typeof(SteamWorkshopDownloader.State),
typeof(LoversLabDownloader.State), typeof(GameFileSourceDownloader.State)
},
};
Config.VersionTolerance.Mode = VersionToleranceMode.Standard;
}
}
}

View File

@ -87,7 +87,7 @@ namespace Wabbajack.Lib
public string Website;
/// <summary>
/// Hash of the readme
/// readme path or website
/// </summary>
public string Readme;
@ -113,6 +113,10 @@ namespace Wabbajack.Lib
.Take(Environment.ProcessorCount)
.Sum(a => a.Size) * 2;
/// <summary>
/// Whether readme is a website
/// </summary>
public bool ReadmeIsWebsite;
}
public class Directive

View File

@ -44,6 +44,15 @@ namespace Wabbajack
.Unit();
}
public static IObservable<Unit> EndingExecution(this IReactiveCommand cmd)
{
return cmd.IsExecuting
.DistinctUntilChanged()
.Pairwise()
.Where(x => x.Previous && !x.Current)
.Unit();
}
/// These snippets were provided by RolandPheasant (author of DynamicData)
/// They'll be going into the official library at some point, but are here for now.
#region Dynamic Data EnsureUniqueChanges

View File

@ -15,11 +15,12 @@ namespace Wabbajack.Lib.LibCefHelpers
{
public static class Helpers
{
private static readonly Task _initTask;
/// <summary>
/// We bundle the cef libs inside the .exe, we need to extract them before loading any wpf code that requires them
/// </summary>
public static void ExtractLibs()
private static async Task ExtractLibs()
{
if (File.Exists("cefglue.7z") && File.Exists("libcef.dll")) return;
@ -30,9 +31,21 @@ namespace Wabbajack.Lib.LibCefHelpers
Utils.Log("Extracting libCef files");
}
using (var wq = new WorkQueue(1))
FileExtractor.ExtractAll(wq, "cefglue.7z", ".");
{
await FileExtractor.ExtractAll(wq, "cefglue.7z", ".");
}
}
static Helpers()
{
_initTask = Task.Run(ExtractLibs);
}
public static Task Initialize()
{
return _initTask;
}
public static HttpClient GetClient(IEnumerable<Cookie> cookies, string referer)
{
var container = ToCookieContainer(cookies);
@ -65,7 +78,6 @@ namespace Wabbajack.Lib.LibCefHelpers
return (await visitor.Task).Where(c => c.Domain.EndsWith(domainEnding)).ToArray();
}
private class CookieVisitor : CefCookieVisitor
{
TaskCompletionSource<List<Cookie>> _source = new TaskCompletionSource<List<Cookie>>();
@ -92,8 +104,6 @@ namespace Wabbajack.Lib.LibCefHelpers
if (disposing)
_source.SetResult(Cookies);
}
}
public class Cookie

View File

@ -117,7 +117,7 @@ namespace Wabbajack.Lib
if (Directory.Exists(lootPath))
{
lootFiles = Directory.EnumerateFiles(lootPath, "userlist.yaml", SearchOption.AllDirectories)
lootFiles = Directory.EnumerateFiles(lootPath, "userlist.yaml", SearchOption.AllDirectories)
.Where(p => p.FileExists())
.Select(p => new RawSourceFile(VFS.Index.ByRootPath[p])
{ Path = Path.Combine(Consts.LOOTFolderFilesDir, p.RelativeTo(lootPath)) });
@ -415,8 +415,9 @@ namespace Wabbajack.Lib
var absolutePaths = AllFiles.ToDictionary(e => e.Path, e => e.AbsolutePath);
await groups.PMap(Queue, group => BuildArchivePatches(group.Key, group, absolutePaths));
if (InstallDirectives.OfType<PatchedFromArchive>().FirstOrDefault(f => f.PatchID == null) != null)
Error("Missing patches after generation, this should not happen");
var firstFailedPatch = InstallDirectives.OfType<PatchedFromArchive>().FirstOrDefault(f => f.PatchID == null);
if (firstFailedPatch != null)
Error($"Missing patches after generation, this should not happen. First failure: {firstFailedPatch.FullPath}");
}
private async Task BuildArchivePatches(string archiveSha, IEnumerable<PatchedFromArchive> group,

View File

@ -318,5 +318,41 @@ namespace Wabbajack.Lib
File.WriteAllText(Path.Combine(OutputFolder, directive.To), data);
}
public static IErrorResponse CheckValidInstallPath(string path)
{
var ret = Utils.IsDirectoryPathValid(path);
if (!ret.Succeeded) return ret;
if (!Directory.Exists(path)) return ErrorResponse.Success;
// Check folder does not have a wabbajack modlist
foreach (var file in Directory.EnumerateFiles(path, DirectoryEnumerationOptions.Recursive))
{
if (!File.Exists(file)) continue;
if (System.IO.Path.GetExtension(file).Equals(ExtensionManager.Extension))
{
return ErrorResponse.Fail($"Cannot install into a folder with a wabbajack modlist inside of it.");
}
}
// Check folder is either empty, or a likely valid previous install
if (!Directory.IsEmpty(path))
{
// Some probably naive check, but should be a good starting point to improve later
if (!Directory.EnumerateFiles(path).Any(file =>
{
var fileName = Path.GetFileName(file);
if (fileName.Equals("ModOrganizer.exe", StringComparison.OrdinalIgnoreCase)) return true;
if (fileName.Equals("ModOrganizer.ini", StringComparison.OrdinalIgnoreCase)) return true;
return false;
}))
{
return ErrorResponse.Fail($"Cannot install into a non-empty folder that does not look like a previous WJ installation.");
}
}
return ErrorResponse.Success;
}
}
}

View File

@ -13,9 +13,9 @@ namespace Wabbajack.Test
protected TestUtils utils { get; set; }
[TestInitialize]
public void TestInitialize()
public async Task TestInitialize()
{
Helpers.ExtractLibs();
await Helpers.Initialize();
Consts.TestMode = true;
utils = new TestUtils();

View File

@ -24,9 +24,9 @@ namespace Wabbajack.Test
public TestContext TestContext { get; set; }
[TestInitialize]
public void Setup()
public async Task Setup()
{
Helpers.ExtractLibs();
await Helpers.Initialize();
Utils.LogMessages.OfType<IInfo>().Subscribe(onNext: msg => TestContext.WriteLine(msg.ShortDescription));
Utils.LogMessages.OfType<IUserIntervention>().Subscribe(msg =>
TestContext.WriteLine("ERROR: User intervetion required: " + msg.ShortDescription));

View File

@ -4,7 +4,6 @@ using System.Reflection;
using System.Windows;
using MahApps.Metro;
using Wabbajack.Common;
using Wabbajack.Lib.LibCefHelpers;
namespace Wabbajack
{
@ -15,7 +14,7 @@ namespace Wabbajack
{
public App()
{
Helpers.ExtractLibs();
// Do initialization in MainWindow ctor
}
}
}

View File

@ -75,6 +75,7 @@ namespace Wabbajack
public string Author { get; set; }
public string Description { get; set; }
public string Website { get; set; }
public bool ReadmeIsWebsite { get; set; }
public string Readme { get; set; }
public string SplashScreen { get; set; }
}

View File

@ -3748,59 +3748,4 @@
<Setter Property="FontSize" Value="14" />
</Style>
<Style BasedOn="{StaticResource MainFilePickerStyle}" TargetType="{x:Type local:FilePicker}" />
<!-- User Intervention Background -->
<Style x:Key="AttentionBorderStyle" TargetType="Border">
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Background" Value="{StaticResource WindowBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource DarkerSecondaryBrush}" />
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsVisible, RelativeSource={RelativeSource Self}}" Value="True" />
<Condition Binding="{Binding IsMouseOver, RelativeSource={RelativeSource Self}}" Value="False" />
</MultiDataTrigger.Conditions>
<MultiDataTrigger.EnterActions>
<BeginStoryboard x:Name="BorderGlow">
<Storyboard>
<ColorAnimation
AutoReverse="True"
RepeatBehavior="Forever"
Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)"
To="{StaticResource Secondary}"
Duration="0:0:1.5" />
</Storyboard>
</BeginStoryboard>
<BeginStoryboard x:Name="BackgroundGlow">
<Storyboard>
<ColorAnimation
AutoReverse="True"
RepeatBehavior="Forever"
Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
To="{StaticResource SecondaryBackground}"
Duration="0:0:1.5" />
</Storyboard>
</BeginStoryboard>
</MultiDataTrigger.EnterActions>
<MultiDataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)"
To="{StaticResource DarkerSecondary}"
Duration="0:0:0.1" />
</Storyboard>
</BeginStoryboard>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
To="{StaticResource WindowBackgroundColor}"
Duration="0:0:0.1" />
</Storyboard>
</BeginStoryboard>
</MultiDataTrigger.ExitActions>
</MultiDataTrigger>
</Style.Triggers>
</Style>
</ResourceDictionary>

View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Wabbajack
{
public class AsyncLazy<T> : Lazy<Task<T>>
{
public AsyncLazy(Func<T> valueFactory) :
base(() => Task.Factory.StartNew(valueFactory))
{
}
public AsyncLazy(Func<Task<T>> taskFactory) :
base(() => Task.Factory.StartNew(() => taskFactory()).Unwrap())
{
}
}
}

View File

@ -10,6 +10,7 @@ using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Windows.Input;
using System.Windows.Media.Imaging;
using Wabbajack.Common;
using Wabbajack.Common.StatusFeed;
@ -46,20 +47,18 @@ namespace Wabbajack
public IReactiveCommand BackCommand { get; }
public IReactiveCommand GoToModlistCommand { get; }
public IReactiveCommand CloseWhenCompleteCommand { get; }
public IReactiveCommand BeginCommand { 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 bool StartedCompilation { get; set; }
[Reactive]
public ErrorResponse? Completed { get; set; }
public CompilerVM(MainWindowVM mainWindowVM)
{
@ -137,7 +136,8 @@ namespace Wabbajack
execute: () =>
{
mainWindowVM.ActivePane = mainWindowVM.ModeSelectionVM;
CompilationMode = false;
StartedCompilation = false;
Completed = null;
},
canExecute: this.WhenAny(x => x.Compiling)
.Select(x => !x));
@ -166,15 +166,6 @@ namespace Wabbajack
.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))
.CombineLatest(
@ -183,21 +174,37 @@ namespace Wabbajack
{
if (compiler == null)
{
return Observable.Return<float>(completed ? 1f : 0f);
return Observable.Return<float>(completed != null ? 1f : 0f);
}
return compiler.PercentCompleted;
return compiler.PercentCompleted.StartWith(0);
})
.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()
BeginCommand = ReactiveCommand.CreateFromTask(
canExecute: this.WhenAny(x => x.Compiler.CanCompile)
.Switch(),
execute: async () =>
{
try
{
await this.Compiler.Compile();
Completed = ErrorResponse.Success;
}
catch (Exception ex)
{
Completed = ErrorResponse.Fail(ex);
while (ex.InnerException != null) ex = ex.InnerException;
Utils.Error(ex, $"Compiler error");
}
});
// When sub compiler begins a compile, mark state variable
BeginCommand.StartingExecution()
.Subscribe(_ =>
{
CompilationMode = true;
StartedCompilation = true;
})
.DisposeWith(CompositeDisposable);
@ -218,14 +225,16 @@ namespace Wabbajack
.ToProperty(this, nameof(ActiveGlobalUserIntervention));
CloseWhenCompleteCommand = ReactiveCommand.Create(
canExecute: this.WhenAny(x => x.Completed),
canExecute: this.WhenAny(x => x.Completed)
.Select(x => x != null),
execute: () =>
{
MWVM.ShutdownApplication();
});
GoToModlistCommand = ReactiveCommand.Create(
canExecute: this.WhenAny(x => x.Completed),
canExecute: this.WhenAny(x => x.Completed)
.Select(x => x != null),
execute: () =>
{
if (string.IsNullOrWhiteSpace(OutputLocation.TargetPath))

View File

@ -1,4 +1,6 @@
using ReactiveUI;
using System;
using System.Threading.Tasks;
using ReactiveUI;
using Wabbajack.Common;
using Wabbajack.Lib;
@ -6,10 +8,10 @@ namespace Wabbajack
{
public interface ISubCompilerVM
{
IReactiveCommand BeginCommand { get; }
ACompiler ActiveCompilation { get; }
ModlistSettingsEditorVM ModlistSettings { get; }
void Unload();
IObservable<bool> CanCompile { get; }
Task Compile();
}
}

View File

@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Wabbajack.Common;
using Wabbajack.Lib;
@ -38,6 +39,8 @@ namespace Wabbajack
[Reactive]
public StatusUpdateTracker StatusTracker { get; private set; }
public IObservable<bool> CanCompile { get; }
public MO2CompilerVM(CompilerVM parent)
{
Parent = parent;
@ -93,8 +96,8 @@ namespace Wabbajack
// Load custom modlist settings per MO2 profile
_modlistSettings = Observable.CombineLatest(
this.WhenAny(x => x.ModListLocation.ErrorState),
this.WhenAny(x => x.ModListLocation.TargetPath),
(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))
@ -119,67 +122,16 @@ namespace Wabbajack
.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),
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(parent.OutputLocation.TargetPath))
{
outputFile = MOProfile + ExtensionManager.Extension;
}
else
{
outputFile = Path.Combine(parent.OutputLocation.TargetPath, MOProfile + ExtensionManager.Extension);
}
ActiveCompilation = new MO2Compiler(
mo2Folder: Mo2Folder,
mo2Profile: MOProfile,
outputFile: outputFile)
{
ModListName = ModlistSettings.ModListName,
ModListAuthor = ModlistSettings.AuthorText,
ModListDescription = ModlistSettings.Description,
ModListImage = ModlistSettings.ImagePath.TargetPath,
ModListWebsite = ModlistSettings.Website,
ModListReadme = ModlistSettings.ReadMeText.TargetPath,
};
}
catch (Exception ex)
{
while (ex.InnerException != null) ex = ex.InnerException;
Utils.Error(ex, $"Compiler error");
return;
}
try
{
await ActiveCompilation.Begin();
}
catch (Exception ex)
{
while (ex.InnerException != null) ex = ex.InnerException;
Utils.Error(ex, $"Compiler error");
}
finally
{
StatusTracker = null;
ActiveCompilation.Dispose();
ActiveCompilation = null;
}
});
CanCompile = Observable.CombineLatest(
this.WhenAny(x => x.ModListLocation.InError),
this.WhenAny(x => x.DownloadLocation.InError),
parent.WhenAny(x => x.OutputLocation.InError),
this.WhenAny(x => x.ModlistSettings)
.Select(x => x?.InError ?? Observable.Return(false))
.Switch(),
resultSelector: (ml, down, output, modlistSettings) => !ml && !down && !output && !modlistSettings)
.Publish()
.RefCount();
// Load settings
_settings = parent.MWVM.Settings.Compiler.MO2Compilation;
@ -193,11 +145,11 @@ namespace Wabbajack
.DisposeWith(CompositeDisposable);
// If Mo2 folder changes and download location is empty, set it for convenience
this.WhenAny(x => x.Mo2Folder)
(this).WhenAny(x => x.Mo2Folder)
.DelayInitial(TimeSpan.FromMilliseconds(100))
.Where(x => Directory.Exists(x))
.FilterSwitch(
this.WhenAny(x => x.DownloadLocation.Exists)
(this).WhenAny(x => x.DownloadLocation.Exists)
.Invert())
// A skip is needed to ignore the initial signal when the FilterSwitch turns on
.Skip(1)
@ -214,5 +166,42 @@ namespace Wabbajack
_settings.LastCompiledProfileLocation = ModListLocation.TargetPath;
ModlistSettings?.Save();
}
public async Task Compile()
{
string outputFile;
if (string.IsNullOrWhiteSpace(Parent.OutputLocation.TargetPath))
{
outputFile = MOProfile + ExtensionManager.Extension;
}
else
{
outputFile = Path.Combine(Parent.OutputLocation.TargetPath, MOProfile + ExtensionManager.Extension);
}
try
{
ActiveCompilation = new MO2Compiler(
mo2Folder: Mo2Folder,
mo2Profile: MOProfile,
outputFile: outputFile)
{
ModListName = ModlistSettings.ModListName,
ModListAuthor = ModlistSettings.AuthorText,
ModListDescription = ModlistSettings.Description,
ModListImage = ModlistSettings.ImagePath.TargetPath,
ModListWebsite = ModlistSettings.Website,
ModListReadme = ModlistSettings.ReadmeIsWebsite ? ModlistSettings.ReadmeWebsite : ModlistSettings.ReadmeFilePath.TargetPath,
ReadmeIsWebsite = ModlistSettings.ReadmeIsWebsite,
};
await ActiveCompilation.Begin();
}
finally
{
StatusTracker = null;
ActiveCompilation.Dispose();
ActiveCompilation = null;
}
}
}
}

View File

@ -1,7 +1,9 @@
using System;
using System.Reactive.Linq;
using System.Windows.Input;
using DynamicData;
using Microsoft.WindowsAPICodePack.Dialogs;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.Lib;
@ -22,13 +24,22 @@ namespace Wabbajack
public FilePickerVM ImagePath { get; }
public FilePickerVM ReadMeText { get; }
public FilePickerVM ReadmeFilePath { get; }
[Reactive]
public string ReadmeWebsite { get; set; }
[Reactive]
public string Website { get; set; }
[Reactive]
public bool ReadmeIsWebsite { get; set; }
public IObservable<bool> InError { get; }
public ICommand SwapToTextReadmeCommand { get; }
public ICommand SwapToWebsiteReadmeCommand { get; }
public ModlistSettingsEditorVM(CompilationModlistSettings settings)
{
this._settings = settings;
@ -38,19 +49,24 @@ namespace Wabbajack
PathType = FilePickerVM.PathTypeOptions.File,
};
ImagePath.Filters.Add(new CommonFileDialogFilter("Banner image", "*.png"));
ReadMeText = new FilePickerVM()
ReadmeFilePath = new FilePickerVM()
{
PathType = FilePickerVM.PathTypeOptions.File,
ExistCheckOption = FilePickerVM.CheckOptions.IfPathNotEmpty,
};
ReadMeText.Filters.Add(new CommonFileDialogFilter("Text", "*.txt"));
ReadmeFilePath.Filters.Add(new CommonFileDialogFilter("Text", "*.txt"));
ReadmeFilePath.Filters.Add(new CommonFileDialogFilter("HTML File", "*.html"));
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)
this.WhenAny(x => x.ReadmeFilePath.ErrorState).Select(err => err.Failed),
this.WhenAny(x => x.ReadmeIsWebsite),
resultSelector: (img, readme, isWebsite) => img || (readme && !isWebsite))
.Publish()
.RefCount();
SwapToTextReadmeCommand = ReactiveCommand.Create(() => ReadmeIsWebsite = false);
SwapToWebsiteReadmeCommand = ReactiveCommand.Create(() => ReadmeIsWebsite = true);
}
public void Init()
@ -61,7 +77,15 @@ namespace Wabbajack
ModListName = _settings.ModListName;
}
Description = _settings.Description;
ReadMeText.TargetPath = _settings.Readme;
ReadmeIsWebsite = _settings.ReadmeIsWebsite;
if (ReadmeIsWebsite)
{
ReadmeWebsite = _settings.Readme;
}
else
{
ReadmeFilePath.TargetPath = _settings.Readme;
}
ImagePath.TargetPath = _settings.SplashScreen;
Website = _settings.Website;
}
@ -71,7 +95,15 @@ namespace Wabbajack
_settings.Author = AuthorText;
_settings.ModListName = ModListName;
_settings.Description = Description;
_settings.Readme = ReadMeText.TargetPath;
_settings.ReadmeIsWebsite = ReadmeIsWebsite;
if (ReadmeIsWebsite)
{
_settings.Readme = ReadmeWebsite;
}
else
{
_settings.Readme = ReadmeFilePath.TargetPath;
}
_settings.SplashScreen = ImagePath.TargetPath;
_settings.Website = Website;
}

View File

@ -54,6 +54,8 @@ namespace Wabbajack
[Reactive]
public StatusUpdateTracker StatusTracker { get; private set; }
public IObservable<bool> CanCompile { get; }
public VortexCompilerVM(CompilerVM parent)
{
Parent = parent;
@ -77,7 +79,7 @@ namespace Wabbajack
};
// Load custom ModList settings when game type changes
_modListSettings = this.WhenAny(x => x.SelectedGame)
_modListSettings = (this).WhenAny(x => x.SelectedGame)
.Select(game =>
{
if (game == null) return null;
@ -97,67 +99,16 @@ namespace Wabbajack
.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),
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: outputFile)
{
ModListName = ModlistSettings.ModListName,
ModListAuthor = ModlistSettings.AuthorText,
ModListDescription = ModlistSettings.Description,
ModListImage = ModlistSettings.ImagePath.TargetPath,
ModListWebsite = ModlistSettings.Website,
ModListReadme = ModlistSettings.ReadMeText.TargetPath,
};
}
catch (Exception ex)
{
while (ex.InnerException != null) ex = ex.InnerException;
Utils.Error(ex, $"Compiler error");
return;
}
await Task.Run(async () =>
{
try
{
await ActiveCompilation.Begin();
}
catch (Exception ex)
{
while (ex.InnerException != null) ex = ex.InnerException;
Utils.Error(ex, $"Compiler error");
}
finally
{
StatusTracker = null;
ActiveCompilation.Dispose();
ActiveCompilation = null;
}
});
});
CanCompile = Observable.CombineLatest(
this.WhenAny(x => x.GameLocation.InError),
this.WhenAny(x => x.DownloadsLocation.InError),
this.WhenAny(x => x.StagingLocation.InError),
this.WhenAny(x => x.ModlistSettings)
.Select(x => x?.InError ?? Observable.Return(false))
.Switch(),
(g, d, s, ml) => !g && !d && !s && !ml)
.Publish()
.RefCount();
// Load settings
_settings = parent.MWVM.Settings.Compiler.VortexCompilation;
@ -167,7 +118,7 @@ namespace Wabbajack
.DisposeWith(CompositeDisposable);
// Load custom game settings when game type changes
this.WhenAny(x => x.SelectedGame)
(this).WhenAny(x => x.SelectedGame)
.Select(game => _settings.ModlistSettings.TryCreate(game.Game))
.Pairwise()
.Subscribe(pair =>
@ -230,5 +181,40 @@ namespace Wabbajack
var gogGame = GOGHandler.Instance.Games.FirstOrDefault(g => g.Game.HasValue && g.Game == SelectedGame.Game);
GameLocation.TargetPath = gogGame?.Path;
}
public async Task Compile()
{
string outputFile = $"{ModlistSettings.ModListName}{ExtensionManager.Extension}";
if (!string.IsNullOrWhiteSpace(Parent.OutputLocation.TargetPath))
{
outputFile = Path.Combine(Parent.OutputLocation.TargetPath, outputFile);
}
try
{
ActiveCompilation = new VortexCompiler(
game: SelectedGame.Game,
gamePath: GameLocation.TargetPath,
vortexFolder: VortexCompiler.TypicalVortexFolder(),
downloadsFolder: DownloadsLocation.TargetPath,
stagingFolder: StagingLocation.TargetPath,
outputFile: outputFile)
{
ModListName = ModlistSettings.ModListName,
ModListAuthor = ModlistSettings.AuthorText,
ModListDescription = ModlistSettings.Description,
ModListImage = ModlistSettings.ImagePath.TargetPath,
ModListWebsite = ModlistSettings.Website,
ModListReadme = ModlistSettings.ReadmeIsWebsite ? ModlistSettings.ReadmeWebsite : ModlistSettings.ReadmeFilePath.TargetPath,
ReadmeIsWebsite = ModlistSettings.ReadmeIsWebsite,
};
await ActiveCompilation.Begin();
}
finally
{
StatusTracker = null;
ActiveCompilation.Dispose();
ActiveCompilation = null;
}
}
}
}

View File

@ -12,11 +12,12 @@ namespace Wabbajack
public interface ISubInstallerVM
{
InstallerVM Parent { get; }
IReactiveCommand BeginCommand { get; }
AInstaller ActiveInstallation { get; }
void Unload();
bool SupportsAfterInstallNavigation { get; }
void AfterInstallNavigation();
int ConfigVisualVerticalOffset { get; }
IObservable<bool> CanInstall { get; }
Task Install();
}
}

View File

@ -46,11 +46,11 @@ namespace Wabbajack
private readonly ObservableAsPropertyHelper<bool> _installing;
public bool Installing => _installing.Value;
/// <summary>
/// Tracks whether installation has begun
/// </summary>
[Reactive]
public bool InstallingMode { get; set; }
public bool StartedInstallation { get; set; }
[Reactive]
public ErrorResponse? Completed { get; set; }
private readonly ObservableAsPropertyHelper<ImageSource> _image;
public ImageSource Image => _image.Value;
@ -82,9 +82,6 @@ namespace Wabbajack
private readonly ObservableAsPropertyHelper<IUserIntervention> _ActiveGlobalUserIntervention;
public IUserIntervention ActiveGlobalUserIntervention => _ActiveGlobalUserIntervention.Value;
private readonly ObservableAsPropertyHelper<bool> _Completed;
public bool Completed => _Completed.Value;
// Command properties
public IReactiveCommand ShowReportCommand { get; }
public IReactiveCommand OpenReadmeCommand { get; }
@ -92,6 +89,7 @@ namespace Wabbajack
public IReactiveCommand BackCommand { get; }
public IReactiveCommand CloseWhenCompleteCommand { get; }
public IReactiveCommand GoToInstallCommand { get; }
public IReactiveCommand BeginCommand { get; }
public InstallerVM(MainWindowVM mainWindowVM)
{
@ -163,7 +161,7 @@ namespace Wabbajack
.Select(modList => modList?.ReportHTML)
.ToProperty(this, nameof(HTMLReport));
_installing = this.WhenAny(x => x.Installer.ActiveInstallation)
.Select(compilation => compilation != null)
.Select(i => i != null)
.ObserveOnGuiThread()
.ToProperty(this, nameof(Installing));
_TargetManager = this.WhenAny(x => x.ModList)
@ -182,21 +180,13 @@ namespace Wabbajack
BackCommand = ReactiveCommand.Create(
execute: () =>
{
InstallingMode = false;
StartedInstallation = false;
Completed = null;
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))
.CombineLatest(
@ -205,9 +195,9 @@ namespace Wabbajack
{
if (installer == null)
{
return Observable.Return<float>(completed ? 1f : 0f);
return Observable.Return<float>(completed != null ? 1f : 0f);
}
return installer.PercentCompleted;
return installer.PercentCompleted.StartWith(0f);
})
.Switch()
.Debounce(TimeSpan.FromMilliseconds(25))
@ -273,7 +263,7 @@ namespace Wabbajack
// Define commands
ShowReportCommand = ReactiveCommand.Create(ShowReport);
OpenReadmeCommand = ReactiveCommand.Create(
execute: OpenReadmeWindow,
execute: () => this.ModList?.OpenReadmeWindow(),
canExecute: this.WhenAny(x => x.ModList)
.Select(modList => !string.IsNullOrEmpty(modList?.Readme))
.ObserveOnGuiThread());
@ -285,11 +275,11 @@ namespace Wabbajack
_progressTitle = Observable.CombineLatest(
this.WhenAny(x => x.Installing),
this.WhenAny(x => x.InstallingMode),
resultSelector: (installing, mode) =>
this.WhenAny(x => x.StartedInstallation),
resultSelector: (installing, started) =>
{
if (!installing) return "Configuring";
return mode ? "Installing" : "Installed";
return started ? "Installing" : "Installed";
})
.ToProperty(this, nameof(ProgressTitle));
@ -317,13 +307,39 @@ namespace Wabbajack
.Subscribe()
.DisposeWith(CompositeDisposable);
BeginCommand = ReactiveCommand.CreateFromTask(
canExecute: this.WhenAny(x => x.Installer.CanInstall)
.Switch(),
execute: async () =>
{
try
{
await this.Installer.Install();
Completed = ErrorResponse.Success;
try
{
this.ModList?.OpenReadmeWindow();
}
catch (Exception ex)
{
Utils.Error(ex);
}
}
catch (Exception ex)
{
while (ex.InnerException != null) ex = ex.InnerException;
Utils.Log(ex.StackTrace);
Utils.Log(ex.ToString());
Utils.Log($"{ex.Message} - Can't continue");
Completed = ErrorResponse.Fail(ex);
}
});
// When sub installer begins an install, mark state variable
this.WhenAny(x => x.Installer.BeginCommand)
.Select(x => x?.StartingExecution() ?? Observable.Empty<Unit>())
.Switch()
BeginCommand.StartingExecution()
.Subscribe(_ =>
{
InstallingMode = true;
StartedInstallation = true;
})
.DisposeWith(CompositeDisposable);
@ -344,7 +360,8 @@ namespace Wabbajack
.ToProperty(this, nameof(ActiveGlobalUserIntervention));
CloseWhenCompleteCommand = ReactiveCommand.Create(
canExecute: this.WhenAny(x => x.Completed),
canExecute: this.WhenAny(x => x.Completed)
.Select(x => x != null),
execute: () =>
{
MWVM.ShutdownApplication();
@ -352,7 +369,8 @@ namespace Wabbajack
GoToInstallCommand = ReactiveCommand.Create(
canExecute: Observable.CombineLatest(
this.WhenAny(x => x.Completed),
this.WhenAny(x => x.Completed)
.Select(x => x != null),
this.WhenAny(x => x.Installer.SupportsAfterInstallNavigation),
resultSelector: (complete, supports) => complete && supports),
execute: () =>
@ -367,31 +385,5 @@ namespace Wabbajack
File.WriteAllText(file, HTMLReport);
Process.Start(file);
}
private void OpenReadmeWindow()
{
if (string.IsNullOrEmpty(ModList.Readme)) return;
using (var fs = new FileStream(ModListLocation.TargetPath, FileMode.Open, FileAccess.Read, FileShare.Read))
using (var ar = new ZipArchive(fs, ZipArchiveMode.Read))
using (var ms = new MemoryStream())
{
var entry = ar.GetEntry(ModList.Readme);
if (entry == null)
{
Utils.Log($"Tried to open a non-existant readme: {ModList.Readme}");
return;
}
using (var e = entry.Open())
{
e.CopyTo(ms);
}
ms.Seek(0, SeekOrigin.Begin);
using (var reader = new StreamReader(ms))
{
var viewer = new TextViewer(reader.ReadToEnd(), ModList.Name);
viewer.Show();
}
}
}
}
}

View File

@ -18,7 +18,7 @@ namespace Wabbajack
{
public InstallerVM Parent { get; }
public IReactiveCommand BeginCommand { get; }
public IObservable<bool> CanInstall { get; }
[Reactive]
public AInstaller ActiveInstallation { get; private set; }
@ -48,7 +48,7 @@ namespace Wabbajack
PromptTitle = "Select Installation Directory",
};
Location.AdditionalError = this.WhenAny(x => x.Location.TargetPath)
.Select(x => Utils.IsDirectoryPathValid(x));
.Select(x => MO2Installer.CheckValidInstallPath(x));
DownloadLocation = new FilePickerVM()
{
ExistCheckOption = FilePickerVM.CheckOptions.Off,
@ -58,61 +58,13 @@ namespace Wabbajack
DownloadLocation.AdditionalError = this.WhenAny(x => x.DownloadLocation.TargetPath)
.Select(x => Utils.IsDirectoryPathValid(x));
BeginCommand = ReactiveCommand.CreateFromTask(
canExecute: Observable.CombineLatest(
this.WhenAny(x => x.Location.InError),
this.WhenAny(x => x.DownloadLocation.InError),
installerVM.WhenAny(x => x.ModListLocation.InError),
resultSelector: (loc, modlist, download) =>
{
return !loc && !download && !modlist;
})
.ObserveOnGuiThread(),
execute: async () =>
CanInstall = Observable.CombineLatest(
this.WhenAny(x => x.Location.InError),
this.WhenAny(x => x.DownloadLocation.InError),
installerVM.WhenAny(x => x.ModListLocation.InError),
resultSelector: (loc, modlist, download) =>
{
AInstaller installer;
try
{
installer = new MO2Installer(
archive: installerVM.ModListLocation.TargetPath,
modList: installerVM.ModList.SourceModList,
outputFolder: Location.TargetPath,
downloadFolder: DownloadLocation.TargetPath);
}
catch (Exception ex)
{
while (ex.InnerException != null) ex = ex.InnerException;
Utils.Log(ex.StackTrace);
Utils.Log(ex.ToString());
Utils.Log($"{ex.Message} - Can't continue");
ActiveInstallation = null;
return;
}
await Task.Run(async () =>
{
IDisposable subscription = null;
try
{
var workTask = installer.Begin();
ActiveInstallation = installer;
await workTask;
}
catch (Exception ex)
{
while (ex.InnerException != null) ex = ex.InnerException;
Utils.Log(ex.StackTrace);
Utils.Log(ex.ToString());
Utils.Log($"{ex.Message} - Can't continue");
}
finally
{
// Dispose of CPU tracking systems
subscription?.Dispose();
ActiveInstallation = null;
}
});
return !loc && !download && !modlist;
});
// Have Installation location updates modify the downloads location if empty
@ -131,7 +83,7 @@ namespace Wabbajack
_CurrentSettings = installerVM.WhenAny(x => x.ModListLocation.TargetPath)
.Select(path => path == null ? null : installerVM.MWVM.Settings.Installer.Mo2ModlistSettings.TryCreate(path))
.ToProperty(this, nameof(CurrentSettings));
this.WhenAny(x => x.CurrentSettings)
(this).WhenAny(x => x.CurrentSettings)
.Pairwise()
.Subscribe(settingsPair =>
{
@ -184,5 +136,29 @@ namespace Wabbajack
{
Process.Start("explorer.exe", Location.TargetPath);
}
public async Task Install()
{
var installer = new MO2Installer(
archive: Parent.ModListLocation.TargetPath,
modList: Parent.ModList.SourceModList,
outputFolder: Location.TargetPath,
downloadFolder: DownloadLocation.TargetPath);
await Task.Run(async () =>
{
try
{
var workTask = installer.Begin();
ActiveInstallation = installer;
await workTask;
return ErrorResponse.Success;
}
finally
{
ActiveInstallation = null;
}
});
}
}
}

View File

@ -15,8 +15,6 @@ namespace Wabbajack
{
public InstallerVM Parent { get; }
public IReactiveCommand BeginCommand { get; }
[Reactive]
public AInstaller ActiveInstallation { get; private set; }
@ -33,6 +31,8 @@ namespace Wabbajack
public int ConfigVisualVerticalOffset => 0;
public IObservable<bool> CanInstall { get; }
public VortexInstallerVM(InstallerVM installerVM)
{
Parent = installerVM;
@ -40,60 +40,11 @@ namespace Wabbajack
_TargetGame = installerVM.WhenAny(x => x.ModList.SourceModList.GameType)
.ToProperty(this, nameof(TargetGame));
BeginCommand = ReactiveCommand.CreateFromTask(
canExecute: Observable.CombineLatest(
this.WhenAny(x => x.TargetGame)
.Select(game => VortexCompiler.IsActiveVortexGame(game)),
installerVM.WhenAny(x => x.ModListLocation.InError),
resultSelector: (isVortexGame, modListErr) => isVortexGame && !modListErr),
execute: async () =>
{
AInstaller installer;
try
{
var download = VortexCompiler.RetrieveDownloadLocation(TargetGame);
var staging = VortexCompiler.RetrieveStagingLocation(TargetGame);
installer = new VortexInstaller(
archive: installerVM.ModListLocation.TargetPath,
modList: installerVM.ModList.SourceModList,
outputFolder: staging,
downloadFolder: download);
}
catch (Exception ex)
{
while (ex.InnerException != null) ex = ex.InnerException;
Utils.Log(ex.StackTrace);
Utils.Log(ex.ToString());
Utils.Log($"{ex.Message} - Can't continue");
ActiveInstallation = null;
return;
}
await Task.Run(async () =>
{
IDisposable subscription = null;
try
{
var workTask = installer.Begin();
ActiveInstallation = installer;
await workTask;
}
catch (Exception ex)
{
while (ex.InnerException != null) ex = ex.InnerException;
Utils.Log(ex.StackTrace);
Utils.Log(ex.ToString());
Utils.Log($"{ex.Message} - Can't continue");
}
finally
{
// Dispose of CPU tracking systems
subscription?.Dispose();
ActiveInstallation = null;
}
});
});
CanInstall = Observable.CombineLatest(
this.WhenAny(x => x.TargetGame)
.Select(game => VortexCompiler.IsActiveVortexGame(game)),
installerVM.WhenAny(x => x.ModListLocation.InError),
resultSelector: (isVortexGame, modListErr) => isVortexGame && !modListErr);
}
public void Unload()
@ -104,5 +55,32 @@ namespace Wabbajack
{
throw new NotImplementedException();
}
public async Task Install()
{
AInstaller installer;
var download = VortexCompiler.RetrieveDownloadLocation(TargetGame);
var staging = VortexCompiler.RetrieveStagingLocation(TargetGame);
installer = new VortexInstaller(
archive: Parent.ModListLocation.TargetPath,
modList: Parent.ModList.SourceModList,
outputFolder: staging,
downloadFolder: download);
await Task.Run(async () =>
{
try
{
var workTask = installer.Begin();
ActiveInstallation = installer;
await workTask;
}
finally
{
ActiveInstallation = null;
}
});
}
}
}

View File

@ -37,7 +37,6 @@ namespace Wabbajack
public readonly Lazy<InstallerVM> Installer;
public readonly Lazy<ModListGalleryVM> Gallery;
public readonly ModeSelectionVM ModeSelectionVM;
public readonly WebBrowserVM WebBrowserVM;
public readonly UserInterventionHandlers UserInterventionHandlers;
public readonly LoginManagerVM LoginManagerVM;
@ -61,7 +60,6 @@ namespace Wabbajack
Compiler = new Lazy<CompilerVM>(() => new CompilerVM(this));
Gallery = new Lazy<ModListGalleryVM>(() => new ModListGalleryVM(this));
ModeSelectionVM = new ModeSelectionVM(this);
WebBrowserVM = new WebBrowserVM();
UserInterventionHandlers = new UserInterventionHandlers(this);
LoginManagerVM = new LoginManagerVM(this);

View File

@ -44,7 +44,7 @@ namespace Wabbajack
Metadata = metadata;
IsBroken = metadata.ValidationSummary.HasFailures;
OpenWebsiteCommand = ReactiveCommand.Create(() => Process.Start($"https://www.wabbajack.org/modlist/{Metadata.Links.MachineURL}"));
ExecuteCommand = ReactiveCommand.CreateFromObservable<Unit, bool>(
ExecuteCommand = ReactiveCommand.CreateFromObservable<Unit, Unit>(
canExecute: this.WhenAny(x => x.IsBroken).Select(x => !x),
execute: (unit) =>
Observable.Return(unit)
@ -63,20 +63,46 @@ namespace Wabbajack
}
return exists;
})
.Where(exists => exists)
// Do any install page swap over on GUI thread
.ObserveOnGuiThread()
.Do(exists =>
.Select(_ =>
{
if (exists)
{
_parent.MWVM.OpenInstaller(Path.GetFullPath(Location));
}
}));
_parent.MWVM.OpenInstaller(Path.GetFullPath(Location));
// Wait for modlist member to be filled, then open its readme
return _parent.MWVM.Installer.Value.WhenAny(x => x.ModList)
.NotNull()
.Take(1)
.Do(modList =>
{
try
{
modList.OpenReadmeWindow();
}
catch (Exception ex)
{
Utils.Error(ex);
}
});
})
.Switch()
.Unit());
_Exists = Observable.Interval(TimeSpan.FromSeconds(0.5))
.Unit()
.StartWith(Unit.Default)
.Select(_ => !metadata.NeedsDownload(Location))
.Select(_ =>
{
try
{
return !metadata.NeedsDownload(Location);
}
catch (Exception)
{
return true;
}
})
.ToProperty(this, nameof(Exists));
}

View File

@ -1,5 +1,6 @@
using ReactiveUI;
using System;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Reactive;
@ -82,5 +83,38 @@ namespace Wabbajack
.Replay(1)
.RefCount();
}
public void OpenReadmeWindow()
{
if (string.IsNullOrEmpty(Readme)) return;
if (SourceModList.ReadmeIsWebsite)
{
Process.Start(Readme);
}
else
{
using (var fs = new FileStream(ModListPath, FileMode.Open, FileAccess.Read, FileShare.Read))
using (var ar = new ZipArchive(fs, ZipArchiveMode.Read))
using (var ms = new MemoryStream())
{
var entry = ar.GetEntry(Readme);
if (entry == null)
{
Utils.Log($"Tried to open a non-existant readme: {Readme}");
return;
}
using (var e = entry.Open())
{
e.CopyTo(ms);
}
ms.Seek(0, SeekOrigin.Begin);
using (var reader = new StreamReader(ms))
{
var viewer = new TextViewer(reader.ReadToEnd(), Name);
viewer.Show();
}
}
}
}
}
}

View File

@ -27,7 +27,7 @@ namespace Wabbajack
{
CancellationTokenSource cancel = new CancellationTokenSource();
var oldPane = MainWindow.ActivePane;
var vm = new WebBrowserVM();
var vm = await WebBrowserVM.GetNew();
MainWindow.ActivePane = vm;
vm.BackCommand = ReactiveCommand.Create(() =>
{

View File

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.Lib;
using Wabbajack.Lib.LibCefHelpers;
using Xilium.CefGlue.WPF;
namespace Wabbajack
@ -20,10 +21,17 @@ namespace Wabbajack
[Reactive]
public IReactiveCommand BackCommand { get; set; }
public WebBrowserVM(string url = "http://www.wabbajack.org")
private WebBrowserVM(string url = "http://www.wabbajack.org")
{
Browser.Address = url;
Instructions = "Wabbajack Web Browser";
}
public static async Task<WebBrowserVM> GetNew(string url = "http://www.wabbajack.org")
{
// Make sure libraries are extracted first
await Helpers.Initialize();
return new WebBrowserVM(url);
}
}
}

View File

@ -0,0 +1,116 @@
<UserControl
x:Class="Wabbajack.AttentionBorder"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Wabbajack"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<Border BorderThickness="1">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="{StaticResource WindowBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource DarkerSecondaryBrush}" />
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsVisible, RelativeSource={RelativeSource Self}}" Value="True" />
<Condition Binding="{Binding IsMouseOver, RelativeSource={RelativeSource Self}}" Value="False" />
<Condition Binding="{Binding Failure, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" Value="False" />
</MultiDataTrigger.Conditions>
<MultiDataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
AutoReverse="True"
RepeatBehavior="Forever"
Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)"
To="{StaticResource Secondary}"
Duration="0:0:1.5" />
</Storyboard>
</BeginStoryboard>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
AutoReverse="True"
RepeatBehavior="Forever"
Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
To="{StaticResource SecondaryBackground}"
Duration="0:0:1.5" />
</Storyboard>
</BeginStoryboard>
</MultiDataTrigger.EnterActions>
<MultiDataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)"
To="{StaticResource DarkerSecondary}"
Duration="0:0:0.1" />
</Storyboard>
</BeginStoryboard>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
To="{StaticResource WindowBackgroundColor}"
Duration="0:0:0.1" />
</Storyboard>
</BeginStoryboard>
</MultiDataTrigger.ExitActions>
</MultiDataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsVisible, RelativeSource={RelativeSource Self}}" Value="True" />
<Condition Binding="{Binding IsMouseOver, RelativeSource={RelativeSource Self}}" Value="False" />
<Condition Binding="{Binding Failure, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" Value="True" />
</MultiDataTrigger.Conditions>
<MultiDataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
AutoReverse="True"
RepeatBehavior="Forever"
Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)"
To="#ff0026"
Duration="0:0:1.5" />
</Storyboard>
</BeginStoryboard>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
AutoReverse="True"
RepeatBehavior="Forever"
Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
To="#540914"
Duration="0:0:1.5" />
</Storyboard>
</BeginStoryboard>
</MultiDataTrigger.EnterActions>
<MultiDataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)"
To="#700d1c"
Duration="0:0:0.1" />
</Storyboard>
</BeginStoryboard>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
To="#1c0307"
Duration="0:0:0.1" />
</Storyboard>
</BeginStoryboard>
</MultiDataTrigger.ExitActions>
</MultiDataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<ContentPresenter Content="{Binding DisplayContent, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" />
</Border>
</UserControl>

View File

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Wabbajack
{
/// <summary>
/// Interaction logic for AttentionBorder.xaml
/// </summary>
public partial class AttentionBorder : UserControl
{
public object DisplayContent
{
get => (object)GetValue(DisplayContentProperty);
set => SetValue(DisplayContentProperty, value);
}
public static readonly DependencyProperty DisplayContentProperty = DependencyProperty.Register(nameof(DisplayContent), typeof(object), typeof(AttentionBorder),
new FrameworkPropertyMetadata(default(object)));
public bool Failure
{
get => (bool)GetValue(FailureProperty);
set => SetValue(FailureProperty, value);
}
public static readonly DependencyProperty FailureProperty = DependencyProperty.Register(nameof(Failure), typeof(bool), typeof(AttentionBorder),
new FrameworkPropertyMetadata(default(bool)));
public AttentionBorder()
{
InitializeComponent();
}
}
}

View File

@ -65,7 +65,7 @@
x:Name="LargeProgressBar"
Grid.Column="0"
Grid.ColumnSpan="4"
Background="#88121212"
Background="#AA121212"
BorderThickness="0"
Maximum="1"
Opacity="{Binding ProgressOpacityPercent, Mode=OneWay, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"

View File

@ -9,93 +9,103 @@
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">
<local:AttentionBorder ClipToBounds="True" Failure="{Binding Completed.Failed}">
<local:AttentionBorder.DisplayContent>
<Grid Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="3*" />
</Grid.RowDefinitions>
<Button
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
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"
Grid.Column="0"
Grid.ColumnSpan="3"
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
VerticalAlignment="Bottom"
FontFamily="Lucida Sans"
FontSize="22"
FontWeight="Black">
<TextBlock.Effect>
<DropShadowEffect BlurRadius="25" Opacity="0.5" />
</TextBlock.Effect>
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="Compilation Complete" />
<Style.Triggers>
<DataTrigger Binding="{Binding Completed.Failed}" Value="True">
<Setter Property="Text" Value="Compilation Failed" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<Grid
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
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}"
@ -117,23 +127,24 @@
<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" />
<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>
</Grid>
</Border>
</local:AttentionBorder.DisplayContent>
</local:AttentionBorder>
</UserControl>

View File

@ -150,12 +150,69 @@
<TextBox Style="{StaticResource ValueStyle}" Text="{Binding Website, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock
Margin="{StaticResource TitleMargin}"
Text="Readme Path"
ToolTip="Path to a readme file." />
<local:FilePicker
PickerVM="{Binding ReadMeText}"
Style="{StaticResource PickerStyle}"
Text="Readme"
ToolTip="Path to a readme file." />
<Grid Margin="0,0,0,6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<local:FilePicker
Grid.Column="0"
Height="27"
Margin="0,0,3,0"
VerticalAlignment="Center"
PickerVM="{Binding ReadmeFilePath}"
ToolTip="Path to a readme file."
Visibility="{Binding ReadmeIsWebsite, Converter={StaticResource bool2VisibilityConverter}, ConverterParameter=False}" />
<TextBox
Grid.Column="0"
Height="27"
Margin="0,0,3,0"
VerticalAlignment="Center"
Text="{Binding ReadmeWebsite}"
ToolTip="Readme website"
Visibility="{Binding ReadmeIsWebsite, Converter={StaticResource bool2VisibilityConverter}}" />
<Button
Grid.Column="1"
Width="27"
Command="{Binding SwapToWebsiteReadmeCommand}"
ToolTip="Set readme to be a website">
<Button.Style>
<Style BasedOn="{StaticResource IconBareButtonStyle}" TargetType="Button">
<Style.Triggers>
<DataTrigger Binding="{Binding ReadmeIsWebsite}" Value="True">
<Setter Property="Foreground" Value="{StaticResource SecondaryBrush}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
<icon:PackIconMaterial
Width="20"
Height="20"
Kind="Web" />
</Button>
<Button
Grid.Column="2"
Width="27"
Command="{Binding SwapToTextReadmeCommand}"
ToolTip="Source readme from a local file (txt | html)">
<Button.Style>
<Style BasedOn="{StaticResource IconBareButtonStyle}" TargetType="Button">
<Style.Triggers>
<DataTrigger Binding="{Binding ReadmeIsWebsite}" Value="False">
<Setter Property="Foreground" Value="{StaticResource SecondaryBrush}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
<icon:PackIconMaterial
Width="20"
Height="20"
Kind="File" />
</Button>
</Grid>
</StackPanel>
</ScrollViewer>
<Border
@ -174,7 +231,7 @@
Margin="35,0,35,0"
VerticalAlignment="Center"
ClipToBounds="False"
Visibility="{Binding CompilationMode, Converter={StaticResource bool2VisibilityConverter}, ConverterParameter=False}">
Visibility="{Binding StartedCompilation, Converter={StaticResource bool2VisibilityConverter}, ConverterParameter=False}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
@ -232,7 +289,7 @@
Grid.Row="0"
Grid.RowSpan="3"
Grid.Column="5"
Command="{Binding Compiler.BeginCommand}" />
Command="{Binding BeginCommand}" />
</Grid>
</Grid>
<Grid
@ -240,7 +297,7 @@
Grid.Column="0"
Grid.ColumnSpan="5"
Margin="5"
Visibility="{Binding CompilationMode, Converter={StaticResource bool2VisibilityConverter}, FallbackValue=Hidden}">
Visibility="{Binding StartedCompilation, Converter={StaticResource bool2VisibilityConverter}, FallbackValue=Hidden}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="4*" />
<ColumnDefinition Width="5" />
@ -251,15 +308,14 @@
Grid.Column="2"
ProgressPercent="{Binding PercentCompleted, Mode=OneWay}"
Visibility="{Binding ActiveGlobalUserIntervention, Converter={StaticResource IsNotNullVisibilityConverter}, ConverterParameter=False}" />
<Border
Grid.Column="2"
Style="{StaticResource AttentionBorderStyle}"
Visibility="{Binding ActiveGlobalUserIntervention, Converter={StaticResource IsNotNullVisibilityConverter}}">
<Grid>
<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}" />
<local:AttentionBorder Grid.Column="2" Visibility="{Binding ActiveGlobalUserIntervention, Converter={StaticResource IsNotNullVisibilityConverter}}">
<local:AttentionBorder.DisplayContent>
<Grid>
<local:ConfirmationInterventionView DataContext="{Binding ActiveGlobalUserIntervention}" Visibility="{Binding ActiveGlobalUserIntervention, Converter={StaticResource IsTypeVisibilityConverter}, ConverterParameter={x:Type common:ConfirmationIntervention}}" />
</Grid>
</local:AttentionBorder.DisplayContent>
</local:AttentionBorder>
<local:CompilationCompleteView Grid.Column="2" Visibility="{Binding Completed, Converter={StaticResource IsNotNullVisibilityConverter}, FallbackValue=Collapsed}" />
</Grid>
</Grid>
</UserControl>

View File

@ -9,136 +9,147 @@
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="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="4"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
FontFamily="Lucida Sans"
FontSize="22"
FontWeight="Black"
Text="Installation Complete">
<TextBlock.Effect>
<DropShadowEffect BlurRadius="25" Opacity="0.5" />
</TextBlock.Effect>
</TextBlock>
<Grid
Grid.Row="1"
Grid.Column="0"
VerticalAlignment="Center">
<local:AttentionBorder ClipToBounds="True" Failure="{Binding Completed.Failed}">
<local:AttentionBorder.DisplayContent>
<Grid Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="3*" />
</Grid.RowDefinitions>
<Button
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="0"
Width="50"
Height="50"
Command="{Binding BackCommand}"
Style="{StaticResource CircleButtonStyle}">
<icon:PackIconMaterial
Width="25"
Height="25"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
Kind="ArrowLeft" />
</Button>
<TextBlock
Grid.Row="1"
Margin="0,10,0,0"
Grid.Column="0"
Grid.ColumnSpan="4"
HorizontalAlignment="Center"
Text="Main Menu" />
</Grid>
<Grid
Grid.Row="1"
Grid.Column="1"
VerticalAlignment="Center"
Visibility="{Binding InstallerSupportsAfterInstallNavigation, Converter={StaticResource bool2VisibilityConverter}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Button
Width="50"
Height="50"
Command="{Binding GoToInstallCommand}"
Style="{StaticResource CircleButtonStyle}">
<icon:PackIconMaterial
Width="23"
Height="23"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
Kind="FolderMove" />
</Button>
<TextBlock
VerticalAlignment="Bottom"
FontFamily="Lucida Sans"
FontSize="22"
FontWeight="Black">
<TextBlock.Effect>
<DropShadowEffect BlurRadius="25" Opacity="0.5" />
</TextBlock.Effect>
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="Installation Complete" />
<Style.Triggers>
<DataTrigger Binding="{Binding Completed.Failed}" Value="True">
<Setter Property="Text" Value="Installation Failed" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<Grid
Grid.Row="1"
Margin="0,10,0,0"
HorizontalAlignment="Center"
Text="Install Folder" />
</Grid>
<Grid
Grid.Row="1"
Grid.Column="2"
VerticalAlignment="Center"
Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Button
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.Column="0"
VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Button
Grid.Row="0"
Width="50"
Height="50"
Command="{Binding BackCommand}"
Style="{StaticResource CircleButtonStyle}">
<icon:PackIconMaterial
Width="25"
Height="25"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
Kind="ArrowLeft" />
</Button>
<TextBlock
Grid.Row="1"
Margin="0,10,0,0"
HorizontalAlignment="Center"
Text="Main Menu" />
</Grid>
<Grid
Grid.Row="1"
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="25"
Height="25"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
Kind="Check" />
</Button>
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
Visibility="{Binding InstallerSupportsAfterInstallNavigation, Converter={StaticResource bool2VisibilityConverter}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Button
Width="50"
Height="50"
Command="{Binding GoToInstallCommand}"
Style="{StaticResource CircleButtonStyle}">
<icon:PackIconMaterial
Width="23"
Height="23"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
Kind="FolderMove" />
</Button>
<TextBlock
Grid.Row="1"
Margin="0,10,0,0"
HorizontalAlignment="Center"
Text="Install Folder" />
</Grid>
<Grid
Grid.Row="1"
Margin="0,10,0,0"
HorizontalAlignment="Center"
Text="Close" />
Grid.Column="2"
VerticalAlignment="Center"
Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Button
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="25"
Height="25"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
Kind="Check" />
</Button>
<TextBlock
Grid.Row="1"
Margin="0,10,0,0"
HorizontalAlignment="Center"
Text="Close" />
</Grid>
</Grid>
</Grid>
</Border>
</local:AttentionBorder.DisplayContent>
</local:AttentionBorder>
</UserControl>

View File

@ -206,7 +206,7 @@
ToolTip="Pause Installation"
Margin="0,0,0,5"
Width="50"
Visibility="{Binding InstallingMode, Converter={StaticResource bool2VisibilityConverter}}">
Visibility="{Binding StartedInstallation, Converter={StaticResource bool2VisibilityConverter}}">
<icon:PackIconMaterial
Kind="Pause" />
</Button>
@ -214,7 +214,7 @@
ToolTip="Stop Installation"
Margin="0,0,0,5"
Width="50"
Visibility="{Binding InstallingMode, Converter={StaticResource bool2VisibilityConverter}}" >
Visibility="{Binding StartedInstallation, Converter={StaticResource bool2VisibilityConverter}}" >
<icon:PackIconFontAwesome
Width="25"
Height="25"
@ -240,7 +240,7 @@
Grid.Row="2"
Margin="5,0,5,5"
ClipToBounds="True"
Visibility="{Binding InstallingMode, Converter={StaticResource bool2VisibilityConverter}, ConverterParameter=False}">
Visibility="{Binding StartedInstallation, Converter={StaticResource bool2VisibilityConverter}, ConverterParameter=False}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="4" />
@ -339,7 +339,7 @@
Margin="5"
VerticalAlignment="Center"
Background="Transparent"
Visibility="{Binding InstallingMode, Converter={StaticResource bool2VisibilityConverter}, ConverterParameter=False}">
Visibility="{Binding StartedInstallation, Converter={StaticResource bool2VisibilityConverter}, ConverterParameter=False}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
@ -388,13 +388,13 @@
Margin="0,0,25,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding Installer.BeginCommand, Mode=OneWay}" />
Command="{Binding BeginCommand, Mode=OneWay}" />
</Grid>
</Grid>
<Grid
Grid.Row="2"
Margin="5,0,5,5"
Visibility="{Binding InstallingMode, Converter={StaticResource bool2VisibilityConverter}, FallbackValue=Hidden}">
Visibility="{Binding StartedInstallation, Converter={StaticResource bool2VisibilityConverter}, FallbackValue=Hidden}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="4*" />
<ColumnDefinition Width="5" />
@ -405,16 +405,15 @@
Grid.Column="2"
ProgressPercent="{Binding PercentCompleted, Mode=OneWay}"
Visibility="{Binding ActiveGlobalUserIntervention, Converter={StaticResource IsNotNullVisibilityConverter}, ConverterParameter=False}" />
<Border
Grid.Column="2"
Style="{StaticResource AttentionBorderStyle}"
Visibility="{Binding ActiveGlobalUserIntervention, Converter={StaticResource IsNotNullVisibilityConverter}}">
<Grid>
<local:ConfirmationInterventionView DataContext="{Binding ActiveGlobalUserIntervention}" Visibility="{Binding ActiveGlobalUserIntervention, Converter={StaticResource IsTypeVisibilityConverter}, ConverterParameter={x:Type common:ConfirmationIntervention}}" />
<local:ConfirmUpdateOfExistingInstallView Visibility="{Binding ActiveGlobalUserIntervention, Converter={StaticResource IsTypeVisibilityConverter}, ConverterParameter={x:Type lib:ConfirmUpdateOfExistingInstall}}" />
</Grid>
</Border>
<local:InstallationCompleteView Grid.Column="2" Visibility="{Binding Completed, Converter={StaticResource bool2VisibilityConverter}, FallbackValue=Collapsed}" />
<local:AttentionBorder Grid.Column="2" Visibility="{Binding ActiveGlobalUserIntervention, Converter={StaticResource IsNotNullVisibilityConverter}}">
<local:AttentionBorder.DisplayContent>
<Grid>
<local:ConfirmationInterventionView DataContext="{Binding ActiveGlobalUserIntervention}" Visibility="{Binding ActiveGlobalUserIntervention, Converter={StaticResource IsTypeVisibilityConverter}, ConverterParameter={x:Type common:ConfirmationIntervention}}" />
<local:ConfirmUpdateOfExistingInstallView Visibility="{Binding ActiveGlobalUserIntervention, Converter={StaticResource IsTypeVisibilityConverter}, ConverterParameter={x:Type lib:ConfirmUpdateOfExistingInstall}}" />
</Grid>
</local:AttentionBorder.DisplayContent>
</local:AttentionBorder>
<local:InstallationCompleteView Grid.Column="2" Visibility="{Binding Completed, Converter={StaticResource IsNotNullVisibilityConverter}, FallbackValue=Collapsed}" />
</Grid>
</Grid>
</UserControl>

View File

@ -1,8 +1,10 @@
using System;
using System.ComponentModel;
using System.Threading.Tasks;
using System.Windows;
using MahApps.Metro.Controls;
using Wabbajack.Common;
using Wabbajack.Lib.LibCefHelpers;
using Application = System.Windows.Application;
using Utils = Wabbajack.Common.Utils;
@ -25,22 +27,26 @@ namespace Wabbajack
Wabbajack.Common.Utils.Error(((Exception)e.ExceptionObject), "Uncaught error");
};
var appPath = System.Reflection.Assembly.GetExecutingAssembly().Location;
try
{
if (!ExtensionManager.IsAssociated() || ExtensionManager.NeedsUpdating(appPath))
{
ExtensionManager.Associate(appPath);
}
}
catch (Exception e)
{
Utils.Log($"ExtensionManager had an exception:\n{e}");
}
Wabbajack.Common.Utils.Log($"Wabbajack Build - {ThisAssembly.Git.Sha}");
// Run some init tasks in background
Task.Run(async () =>
{
await Helpers.Initialize();
var appPath = System.Reflection.Assembly.GetExecutingAssembly().Location;
try
{
if (!ExtensionManager.IsAssociated() || ExtensionManager.NeedsUpdating(appPath))
{
ExtensionManager.Associate(appPath);
}
}
catch (Exception e)
{
Utils.Log($"ExtensionManager had an exception:\n{e}");
}
}).FireAndForget();
// Load settings
string[] args = Environment.GetCommandLineArgs();
if ((args.Length > 1 && args[1] == "nosettings")
@ -58,6 +64,18 @@ namespace Wabbajack
// Set datacontext
_mwvm = new MainWindowVM(this, _settings);
DataContext = _mwvm;
// Bring window to the front if it isn't already
this.Initialized += (s, e) =>
{
this.Activate();
this.Topmost = true;
this.Focus();
};
this.ContentRendered += (s, e) =>
{
this.Topmost = false;
};
}
public void Init(MainWindowVM vm, MainSettings settings)

View File

@ -172,11 +172,15 @@
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</ApplicationDefinition>
<Compile Include="Views\Common\AttentionBorder.xaml.cs">
<DependentUpon>AttentionBorder.xaml</DependentUpon>
</Compile>
<Compile Include="Converters\IsTypeVisibilityConverter.cs" />
<Compile Include="UnderMaintenanceOverlay.xaml.cs">
<DependentUpon>UnderMaintenanceOverlay.xaml</DependentUpon>
</Compile>
<Compile Include="UserInterventions\ShowLoginManager.cs" />
<Compile Include="Util\AsyncLazy.cs" />
<Compile Include="View Models\CPUDisplayVM.cs" />
<Compile Include="View Models\LoginManagerVM.cs" />
<Compile Include="Views\Compilers\CompilationCompleteView.xaml.cs">
@ -278,6 +282,10 @@
<Compile Include="Views\WebBrowserView.xaml.cs">
<DependentUpon>WebBrowserView.xaml</DependentUpon>
</Compile>
<Page Include="Views\Common\AttentionBorder.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UnderMaintenanceOverlay.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>