From ce810dafabd9252b661a7cfa416f57130c7337d0 Mon Sep 17 00:00:00 2001 From: Macocian Alexandru Victor Date: Tue, 10 Dec 2024 14:43:07 +0100 Subject: [PATCH] Support for toolbox builds (#908) Closes #866 --- .../Controls/Buttons/GWToolboxButton.xaml | 23 ++ .../Controls/Buttons/GWToolboxButton.xaml.cs | 30 ++ Daybreak/Controls/Glyphs/GWToolboxGlyph.xaml | 14 +- .../Templates/BuildEntryTemplate.xaml | 10 +- Daybreak/Models/Guildwars/BuildEntryBase.cs | 38 +++ Daybreak/Services/Toolbox/IToolboxService.cs | 12 +- Daybreak/Services/Toolbox/ToolboxService.cs | 274 +++++++++++++++++- Daybreak/Views/BuildsListView.xaml | 7 +- Daybreak/Views/BuildsListView.xaml.cs | 25 +- Daybreak/Views/TeamBuildTemplateView.xaml | 10 +- Daybreak/Views/TeamBuildTemplateView.xaml.cs | 32 +- 11 files changed, 461 insertions(+), 14 deletions(-) create mode 100644 Daybreak/Controls/Buttons/GWToolboxButton.xaml create mode 100644 Daybreak/Controls/Buttons/GWToolboxButton.xaml.cs diff --git a/Daybreak/Controls/Buttons/GWToolboxButton.xaml b/Daybreak/Controls/Buttons/GWToolboxButton.xaml new file mode 100644 index 00000000..39430287 --- /dev/null +++ b/Daybreak/Controls/Buttons/GWToolboxButton.xaml @@ -0,0 +1,23 @@ + + + + + + + + + + + diff --git a/Daybreak/Controls/Buttons/GWToolboxButton.xaml.cs b/Daybreak/Controls/Buttons/GWToolboxButton.xaml.cs new file mode 100644 index 00000000..92d18663 --- /dev/null +++ b/Daybreak/Controls/Buttons/GWToolboxButton.xaml.cs @@ -0,0 +1,30 @@ +using System; +using System.Windows.Controls; +using System.Windows.Extensions; +using System.Windows.Input; + +namespace Daybreak.Controls.Buttons; +/// +/// Interaction logic for GWToolboxButton.xaml +/// +public partial class GWToolboxButton : UserControl +{ + public event EventHandler? Clicked; + + [GenerateDependencyProperty] + private ICommand click = default!; + + public GWToolboxButton() + { + this.InitializeComponent(); + } + + private void HighlightButton_Clicked(object sender, EventArgs e) + { + this.Clicked?.Invoke(this, e); + if (this.Click?.CanExecute(e) is true) + { + this.Click?.Execute(e); + } + } +} diff --git a/Daybreak/Controls/Glyphs/GWToolboxGlyph.xaml b/Daybreak/Controls/Glyphs/GWToolboxGlyph.xaml index badb75da..753144bd 100644 --- a/Daybreak/Controls/Glyphs/GWToolboxGlyph.xaml +++ b/Daybreak/Controls/Glyphs/GWToolboxGlyph.xaml @@ -8,6 +8,18 @@ x:Name="_this" d:DesignHeight="450" d:DesignWidth="800"> - + + + + + + + + + + diff --git a/Daybreak/Controls/Templates/BuildEntryTemplate.xaml b/Daybreak/Controls/Templates/BuildEntryTemplate.xaml index c118f740..96c5251d 100644 --- a/Daybreak/Controls/Templates/BuildEntryTemplate.xaml +++ b/Daybreak/Controls/Templates/BuildEntryTemplate.xaml @@ -8,7 +8,15 @@ mc:Ignorable="d" xmlns:controls="clr-namespace:Daybreak.Controls" d:DesignHeight="450" d:DesignWidth="800"> + + + + diff --git a/Daybreak/Models/Guildwars/BuildEntryBase.cs b/Daybreak/Models/Guildwars/BuildEntryBase.cs index 62c71370..107435d8 100644 --- a/Daybreak/Models/Guildwars/BuildEntryBase.cs +++ b/Daybreak/Models/Guildwars/BuildEntryBase.cs @@ -12,6 +12,44 @@ public abstract class BuildEntryBase : INotifyPropertyChanged, IBuildEntry public Dictionary? Metadata { get; set; } public string? PreviousName { get; set; } + public int? ToolboxBuildId + { + get + { + if (this.Metadata?.TryGetValue(nameof(this.ToolboxBuildId), out var toolBoxBuildString) is true && + int.TryParse(toolBoxBuildString, out var toolboxBuild)) + { + return toolboxBuild; + } + + return default; + } + set + { + this.Metadata ??= []; + this.Metadata[nameof(this.ToolboxBuildId)] = value.HasValue ? value.Value.ToString() : string.Empty; + this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.ToolboxBuildId))); + } + } + public bool IsToolboxBuild + { + get + { + if (this.Metadata?.TryGetValue(nameof(this.IsToolboxBuild), out var toolBoxBuildString) is true && + bool.TryParse(toolBoxBuildString, out var toolboxBuild)) + { + return toolboxBuild; + } + + return false; + } + set + { + this.Metadata ??= []; + this.Metadata[nameof(this.IsToolboxBuild)] = value.ToString(); + this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.IsToolboxBuild))); + } + } public DateTimeOffset CreationTime { get diff --git a/Daybreak/Services/Toolbox/IToolboxService.cs b/Daybreak/Services/Toolbox/IToolboxService.cs index 1694d906..e5e5a29d 100644 --- a/Daybreak/Services/Toolbox/IToolboxService.cs +++ b/Daybreak/Services/Toolbox/IToolboxService.cs @@ -1,5 +1,7 @@ -using Daybreak.Models.Progress; +using Daybreak.Models.Builds; +using Daybreak.Models.Progress; using Daybreak.Services.Mods; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -13,4 +15,12 @@ public interface IToolboxService : IModService Task NotifyUserIfUpdateAvailable(CancellationToken cancellationToken); Task SetupToolbox(ToolboxInstallationStatus toolboxInstallationStatus); + + IAsyncEnumerable GetToolboxBuilds(CancellationToken cancellationToken); + + Task SaveToolboxBuild(TeamBuildEntry teamBuildEntry, CancellationToken cancellationToken); + + Task DeleteToolboxBuild(TeamBuildEntry teamBuildEntry, CancellationToken cancellationToken); + + Task ExportBuildToToolbox(TeamBuildEntry teamBuildEntry, CancellationToken cancellationToken); } diff --git a/Daybreak/Services/Toolbox/ToolboxService.cs b/Daybreak/Services/Toolbox/ToolboxService.cs index 38f36ac7..c5694921 100644 --- a/Daybreak/Services/Toolbox/ToolboxService.cs +++ b/Daybreak/Services/Toolbox/ToolboxService.cs @@ -1,8 +1,10 @@ using Daybreak.Configuration.Options; using Daybreak.Exceptions; using Daybreak.Models; +using Daybreak.Models.Builds; using Daybreak.Models.Mods; using Daybreak.Models.Progress; +using Daybreak.Services.BuildTemplates; using Daybreak.Services.Injection; using Daybreak.Services.Notifications; using Daybreak.Services.Scanner; @@ -12,6 +14,7 @@ using Daybreak.Utils; using Microsoft.Extensions.Logging; using Microsoft.Win32; +using PeNet.Header.Resource; using System; using System.Collections.Generic; using System.Configuration; @@ -20,6 +23,9 @@ using System.Extensions; using System.Extensions.Core; using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -28,6 +34,7 @@ namespace Daybreak.Services.Toolbox; internal sealed class ToolboxService : IToolboxService { private const string ToolboxDestinationDirectorySubPath = "GWToolbox"; + private const string ToolboxBuildsFileName = "builds.ini"; private static readonly string ToolboxDestinationDirectoryPath = PathUtils.GetAbsolutePathFromRoot(ToolboxDestinationDirectorySubPath); private static readonly string UsualToolboxFolderLocation = Path.GetFullPath( @@ -35,7 +42,7 @@ internal sealed class ToolboxService : IToolboxService private static readonly string UsualToolboxLocation = Path.GetFullPath( Path.Combine(UsualToolboxFolderLocation, "GWToolboxdll.dll")); - private readonly IGuildwarsMemoryCache guildwarsMemoryCache; + private readonly IBuildTemplateManager buildTemplateManager; private readonly INotificationService notificationService; private readonly IProcessInjector processInjector; private readonly IToolboxClient toolboxClient; @@ -55,14 +62,14 @@ public bool IsEnabled public bool IsInstalled => File.Exists(this.toolboxOptions.Value.DllPath); public ToolboxService( - IGuildwarsMemoryCache guildwarsMemoryCache, + IBuildTemplateManager buildTemplateManager, INotificationService notificationService, IProcessInjector processInjector, IToolboxClient toolboxClient, ILiveUpdateableOptions toolboxOptions, ILogger logger) { - this.guildwarsMemoryCache = guildwarsMemoryCache.ThrowIfNull(); + this.buildTemplateManager = buildTemplateManager.ThrowIfNull(); this.notificationService = notificationService.ThrowIfNull(); this.processInjector = processInjector.ThrowIfNull(); this.toolboxClient = toolboxClient.ThrowIfNull(); @@ -141,6 +148,137 @@ public async Task SetupToolbox(ToolboxInstallationStatus toolboxInstallati return true; } + public async IAsyncEnumerable GetToolboxBuilds([EnumeratorCancellation] CancellationToken cancellationToken) + { + var toolboxBuildsFile = Path.Combine(UsualToolboxFolderLocation, Environment.MachineName, ToolboxBuildsFileName); + if (!File.Exists(toolboxBuildsFile)) + { + yield break; + } + + using var textReader = new StreamReader(toolboxBuildsFile); + await foreach (var build in this.ParseToolboxBuilds(cancellationToken, textReader)) + { + yield return build; + } + } + + public async Task SaveToolboxBuild(TeamBuildEntry teamBuildEntry, CancellationToken cancellationToken) + { + if (!teamBuildEntry.IsToolboxBuild) + { + return false; + } + + if (!teamBuildEntry.ToolboxBuildId.HasValue) + { + return false; + } + + var toolboxBuildsFile = Path.Combine(UsualToolboxFolderLocation, Environment.MachineName, ToolboxBuildsFileName); + if (!File.Exists(toolboxBuildsFile)) + { + return false; + } + + var buildsToSave = new List(); + using var textReader = new StreamReader(new FileStream(toolboxBuildsFile, FileMode.Open, FileAccess.Read)); + await foreach(var build in this.ParseToolboxBuilds(cancellationToken, textReader)) + { + if (!build.ToolboxBuildId.HasValue) + { + continue; + } + + if (build.ToolboxBuildId.Value == teamBuildEntry.ToolboxBuildId.Value) + { + buildsToSave.Add(teamBuildEntry); + } + else + { + buildsToSave.Add(build); + } + } + + textReader.Close(); + textReader.Dispose(); + using var textWriter = new StreamWriter(new FileStream(toolboxBuildsFile, FileMode.Create, FileAccess.Write)); + return await this.WriteBuildsToStream(buildsToSave, textWriter, cancellationToken); + } + + public async Task DeleteToolboxBuild(TeamBuildEntry teamBuildEntry, CancellationToken cancellationToken) + { + if (!teamBuildEntry.IsToolboxBuild) + { + return false; + } + + if (!teamBuildEntry.ToolboxBuildId.HasValue) + { + return false; + } + + var toolboxBuildsFile = Path.Combine(UsualToolboxFolderLocation, Environment.MachineName, ToolboxBuildsFileName); + if (!File.Exists(toolboxBuildsFile)) + { + return false; + } + + var buildsToSave = new List(); + using var textReader = new StreamReader(new FileStream(toolboxBuildsFile, FileMode.Open, FileAccess.Read)); + await foreach (var build in this.ParseToolboxBuilds(cancellationToken, textReader)) + { + if (!build.ToolboxBuildId.HasValue) + { + continue; + } + + if (build.ToolboxBuildId.Value == teamBuildEntry.ToolboxBuildId.Value) + { + continue; + } + + buildsToSave.Add(build); + } + + textReader.Close(); + textReader.Dispose(); + using var textWriter = new StreamWriter(new FileStream(toolboxBuildsFile, FileMode.Create, FileAccess.Write)); + return await this.WriteBuildsToStream(buildsToSave, textWriter, cancellationToken); + } + + public async Task ExportBuildToToolbox(TeamBuildEntry teamBuildEntry, CancellationToken cancellationToken) + { + if (teamBuildEntry.IsToolboxBuild) + { + return await this.SaveToolboxBuild(teamBuildEntry, cancellationToken); + } + + var toolboxBuildsFile = Path.Combine(UsualToolboxFolderLocation, Environment.MachineName, ToolboxBuildsFileName); + if (!File.Exists(toolboxBuildsFile)) + { + return false; + } + + var buildsToSave = new List(); + using var textReader = new StreamReader(new FileStream(toolboxBuildsFile, FileMode.Open, FileAccess.Read)); + await foreach (var build in this.ParseToolboxBuilds(cancellationToken, textReader)) + { + if (!build.ToolboxBuildId.HasValue) + { + continue; + } + + buildsToSave.Add(build); + } + + buildsToSave.Add(teamBuildEntry); + textReader.Close(); + textReader.Dispose(); + using var textWriter = new StreamWriter(new FileStream(toolboxBuildsFile, FileMode.Create, FileAccess.Write)); + return await this.WriteBuildsToStream(buildsToSave, textWriter, cancellationToken); + } + private async Task SetupToolboxDll(ToolboxInstallationStatus toolboxInstallationStatus) { var scopedLogger = this.logger.CreateScopedLogger(); @@ -238,4 +376,134 @@ private async Task IsUpdateAvailable(CancellationToken cancellationToken) return false; } + + private async IAsyncEnumerable ParseToolboxBuilds([EnumeratorCancellation] CancellationToken cancellationToken, TextReader textReader) + { + TeamBuildEntry? teamBuild = default; + while (await textReader.ReadLineAsync(cancellationToken) is string headerLine) + { + if (!headerLine.StartsWith('[') || + !headerLine.EndsWith(']') || + !headerLine.Contains("builds")) + { + continue; + } + + if (!int.TryParse(headerLine.Replace("[builds", "").Replace("]", ""), out var buildId)) + { + continue; + } + + var loadedBuild = false; + while (await textReader.ReadLineAsync(cancellationToken) is string teamBuildLine) + { + if (string.IsNullOrWhiteSpace(teamBuildLine)) + { + break; + } + + var equalSignIndex = teamBuildLine.IndexOf('='); + var propertyName = teamBuildLine.Substring(0, equalSignIndex).Trim(' '); + var propertyValue = teamBuildLine.Substring(equalSignIndex + 1, teamBuildLine.Length - equalSignIndex - 1).Trim(' '); + if (propertyName is "buildname") + { + teamBuild = this.buildTemplateManager.CreateTeamBuild(propertyValue); + teamBuild.Builds.Clear(); + teamBuild.IsToolboxBuild = true; + teamBuild.ToolboxBuildId = buildId; + } + + if (teamBuild is null) + { + continue; + } + + if (propertyName is "count" && + int.TryParse(propertyValue, out var buildsCount) && + buildsCount > 0) + { + var buildsBuffer = new (string Template, string Name)[buildsCount]; + for (var i = 0; i < buildsCount; i++) + { + buildsBuffer[i] = (string.Empty, string.Empty); + } + + while (await textReader.ReadLineAsync(cancellationToken) is string singleBuildLine) + { + if (string.IsNullOrWhiteSpace(singleBuildLine)) + { + break; + } + + equalSignIndex = singleBuildLine.IndexOf('='); + propertyName = singleBuildLine.Substring(0, equalSignIndex).Trim(' '); + propertyValue = singleBuildLine.Substring(equalSignIndex + 1, singleBuildLine.Length - equalSignIndex - 1); + if (propertyName.StartsWith("name") && + int.TryParse(propertyName.Replace("name", ""), out var singleBuildNameIndex)) + { + buildsBuffer[singleBuildNameIndex].Name = propertyValue; + } + else if (propertyName.StartsWith("template") && + int.TryParse(propertyName.Replace("template", ""), out var singleBuildTemplateIndex)) + { + buildsBuffer[singleBuildTemplateIndex].Template = propertyValue; + } + } + + if (buildsBuffer.Length == 0) + { + continue; + } + + foreach ((var buildTemplate, var buildName) in buildsBuffer) + { + if (!this.buildTemplateManager.TryDecodeTemplate(buildTemplate, out var build) || + build is not SingleBuildEntry singleBuild) + { + continue; + } + + loadedBuild = true; + singleBuild.Name = buildName; + teamBuild?.Builds.Add(singleBuild); + } + } + + if (loadedBuild) + { + break; + } + } + + if (teamBuild is not null) + { + yield return teamBuild; + teamBuild = default; + } + } + } + + private async Task WriteBuildsToStream(List builds, TextWriter textWriter, CancellationToken cancellationToken) + { + for (var i = 0; i < builds.Count; i++) + { + var build = builds[i]; + var stringBuilder = new StringBuilder(); + stringBuilder.AppendLine($"[builds{i:D3}]"); + stringBuilder.AppendLine($"buildname = {build.Name}"); + stringBuilder.AppendLine($"showNumbers = true"); + stringBuilder.AppendLine($"count = {build.Builds.Count}"); + for (var j = 0; j < build.Builds.Count; j++) + { + var singleBuild = build.Builds[j]; + stringBuilder.AppendLine($"name{j} = {singleBuild.Name}"); + stringBuilder.AppendLine($"template{j} = {this.buildTemplateManager.EncodeTemplate(singleBuild)}"); + } + + stringBuilder.AppendLine(); + await textWriter.WriteLineAsync(stringBuilder, cancellationToken); + } + + return true; + } } diff --git a/Daybreak/Views/BuildsListView.xaml b/Daybreak/Views/BuildsListView.xaml index 22b5997c..6d275c2b 100644 --- a/Daybreak/Views/BuildsListView.xaml +++ b/Daybreak/Views/BuildsListView.xaml @@ -124,9 +124,14 @@ + + Visibility="{Binding ElementName=_this, Path=Loading, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}" /> diff --git a/Daybreak/Views/BuildsListView.xaml.cs b/Daybreak/Views/BuildsListView.xaml.cs index 9eaacb7f..5af9ca6e 100644 --- a/Daybreak/Views/BuildsListView.xaml.cs +++ b/Daybreak/Views/BuildsListView.xaml.cs @@ -1,12 +1,16 @@ using Daybreak.Models; using Daybreak.Models.Builds; +using Daybreak.Models.Guildwars; using Daybreak.Services.BuildTemplates; using Daybreak.Services.Navigation; +using Daybreak.Services.Toolbox; using Daybreak.Utils; using System; using System.Collections.Generic; +using System.Core.Extensions; using System.Extensions; using System.Linq; +using System.Threading; using System.Windows.Controls; using System.Windows.Extensions; @@ -17,6 +21,7 @@ namespace Daybreak.Views; /// public partial class BuildsListView : UserControl { + private readonly IToolboxService toolboxService; private readonly IViewManager viewManager; private readonly IBuildTemplateManager buildTemplateManager; @@ -29,10 +34,12 @@ public partial class BuildsListView : UserControl public BuildsListView( IViewManager viewManager, + IToolboxService toolboxService, IBuildTemplateManager buildTemplateManager) { - this.viewManager = viewManager.ThrowIfNull(nameof(viewManager)); - this.buildTemplateManager = buildTemplateManager.ThrowIfNull(nameof(buildTemplateManager)); + this.viewManager = viewManager.ThrowIfNull(); + this.toolboxService = toolboxService.ThrowIfNull(); + this.buildTemplateManager = buildTemplateManager.ThrowIfNull(); this.InitializeComponent(); this.LoadBuilds(); } @@ -41,7 +48,8 @@ private async void LoadBuilds() { this.Loading = true; this.buildEntries = await this.buildTemplateManager.GetBuilds().ToListAsync(); - this.BuildEntries.ClearAnd().AddRange(this.buildEntries.OrderBy(b => b.Name)); + var toolboxBuildEntries = await this.toolboxService.GetToolboxBuilds(CancellationToken.None).ToListAsync(); + this.BuildEntries.ClearAnd().AddRange(this.buildEntries.OrderBy(b => b.Name)).AddRange(toolboxBuildEntries.OrderBy(b => b.Name)); this.Loading = false; this.SearchTextBox.FocusOnTextBox(); } @@ -60,7 +68,16 @@ private void AddTeamButton_Clicked(object sender, EventArgs e) private void BuildEntryTemplate_RemoveClicked(object _, IBuildEntry e) { - this.buildTemplateManager.RemoveBuild(e); + if (e is TeamBuildEntry teamBuild && + teamBuild.IsToolboxBuild) + { + this.toolboxService.DeleteToolboxBuild(teamBuild, CancellationToken.None); + } + else + { + this.buildTemplateManager.RemoveBuild(e); + } + this.LoadBuilds(); } diff --git a/Daybreak/Views/TeamBuildTemplateView.xaml b/Daybreak/Views/TeamBuildTemplateView.xaml index ce0b991b..bb154f46 100644 --- a/Daybreak/Views/TeamBuildTemplateView.xaml +++ b/Daybreak/Views/TeamBuildTemplateView.xaml @@ -8,11 +8,13 @@ xmlns:converters="clr-namespace:Daybreak.Converters" xmlns:templates="clr-namespace:Daybreak.Controls.Templates" xmlns:buttons="clr-namespace:Daybreak.Controls.Buttons" + xmlns:glyphs="clr-namespace:Daybreak.Controls.Glyphs" mc:Ignorable="d" x:Name="_this" d:DesignHeight="450" d:DesignWidth="800"> + @@ -27,6 +29,7 @@ + + + diff --git a/Daybreak/Views/TeamBuildTemplateView.xaml.cs b/Daybreak/Views/TeamBuildTemplateView.xaml.cs index 476e685a..c391b1ce 100644 --- a/Daybreak/Views/TeamBuildTemplateView.xaml.cs +++ b/Daybreak/Views/TeamBuildTemplateView.xaml.cs @@ -4,11 +4,13 @@ using Daybreak.Services.BuildTemplates; using Daybreak.Services.Navigation; using Daybreak.Services.Notifications; +using Daybreak.Services.Toolbox; using Microsoft.Extensions.Logging; using System; using System.Core.Extensions; using System.Extensions; using System.Linq; +using System.Threading; using System.Windows; using System.Windows.Controls; using System.Windows.Extensions; @@ -22,6 +24,7 @@ public partial class TeamBuildTemplateView : UserControl { private const string DisallowedChars = "\r\n/."; + private readonly IToolboxService toolboxService; private readonly INotificationService notificationService; private readonly IViewManager viewManager; private readonly IBuildTemplateManager buildTemplateManager; @@ -44,11 +47,13 @@ public partial class TeamBuildTemplateView : UserControl private string currentBuildSubCode = string.Empty; public TeamBuildTemplateView( + IToolboxService toolboxService, INotificationService notificationService, IViewManager viewManager, IBuildTemplateManager buildTemplateManager, ILogger logger) { + this.toolboxService = toolboxService.ThrowIfNull(); this.notificationService = notificationService.ThrowIfNull(); this.buildTemplateManager = buildTemplateManager.ThrowIfNull(); this.logger = logger.ThrowIfNull(); @@ -199,11 +204,34 @@ private void BuildTemplate_BuildChanged(object sender, EventArgs e) { } } + private void BackButton_Clicked(object sender, EventArgs e) { this.viewManager.ShowView(); } - private void SaveButton_Clicked(object sender, EventArgs e) + + private async void SaveButton_Clicked(object sender, EventArgs e) + { + if (this.CurrentBuild is null) + { + this.viewManager.ShowView(); + return; + } + + this.CurrentBuild.SourceUrl = this.CurrentBuildSource; + if (this.CurrentBuild.IsToolboxBuild) + { + await this.toolboxService.SaveToolboxBuild(this.CurrentBuild, CancellationToken.None); + } + else + { + this.buildTemplateManager.SaveBuild(this.CurrentBuild); + } + + this.viewManager.ShowView(); + } + + private async void ExportButton_Clicked(object sender, EventArgs e) { if (this.CurrentBuild is null) { @@ -212,7 +240,7 @@ private void SaveButton_Clicked(object sender, EventArgs e) } this.CurrentBuild.SourceUrl = this.CurrentBuildSource; - this.buildTemplateManager.SaveBuild(this.CurrentBuild); + await this.toolboxService.ExportBuildToToolbox(this.CurrentBuild, CancellationToken.None); this.viewManager.ShowView(); }