From 1540e6cb7a626e57a55016131dc8656866772301 Mon Sep 17 00:00:00 2001 From: Jeddunk Date: Sun, 10 Jan 2021 16:08:12 +0100 Subject: [PATCH] Added new class GoldbergConfiguration. Global settings can now be saved and reset. Language can now be edited (globally). If Cache is broken, try to forcibly update it from the API. --- GoldbergGUI.Core/Models/GoldbergModel.cs | 13 ++ GoldbergGUI.Core/Services/GoldbergService.cs | 145 ++++++++++++++----- GoldbergGUI.Core/Services/SteamService.cs | 39 +++-- GoldbergGUI.Core/ViewModels/MainViewModel.cs | 138 +++++++++++++----- GoldbergGUI.WPF/Views/MainView.xaml | 4 + 5 files changed, 252 insertions(+), 87 deletions(-) create mode 100644 GoldbergGUI.Core/Models/GoldbergModel.cs diff --git a/GoldbergGUI.Core/Models/GoldbergModel.cs b/GoldbergGUI.Core/Models/GoldbergModel.cs new file mode 100644 index 0000000..724ce27 --- /dev/null +++ b/GoldbergGUI.Core/Models/GoldbergModel.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace GoldbergGUI.Core.Models +{ + public class GoldbergConfiguration + { + public int AppId { get; set; } + public List DlcList { get; set; } + public bool Offline { get; set; } + public bool DisableNetworking { get; set; } + public bool DisableOverlay { get; set; } + } +} \ No newline at end of file diff --git a/GoldbergGUI.Core/Services/GoldbergService.cs b/GoldbergGUI.Core/Services/GoldbergService.cs index 2fc4bce..5e42202 100644 --- a/GoldbergGUI.Core/Services/GoldbergService.cs +++ b/GoldbergGUI.Core/Services/GoldbergService.cs @@ -17,29 +17,35 @@ namespace GoldbergGUI.Core.Services // does file copy stuff public interface IGoldbergService { - public Task<(string accountName, long userSteamId)> Initialize(IMvxLog log); - public Task<(int appId, List dlcList, bool offline, bool disableNetworking, bool disableOverlay)> - Read(string path); - public Task Save(string path, int appId, List dlcList, - bool offline, bool disableNetworking, bool disableOverlay); + public Task<(string accountName, long userSteamId, string language)> Initialize(IMvxLog log); + public Task Read(string path); + public Task Save(string path, GoldbergConfiguration configuration); + public Task<(string accountName, long steamId, string language)> GetGlobalSettings(); + public Task SetGlobalSettings(string accountName, long userSteamId, string language); public bool GoldbergApplied(string path); public Task Download(); public Task Extract(string archivePath); public Task GenerateInterfacesFile(string filePath); + public List Languages(); } - + // ReSharper disable once UnusedType.Global public class GoldbergService : IGoldbergService { private IMvxLog _log; private const string GoldbergUrl = "https://mr_goldberg.gitlab.io/goldberg_emulator/"; + private const string DefaultLanguage = "english"; private readonly string _goldbergZipPath = Path.Combine(Directory.GetCurrentDirectory(), "goldberg.zip"); private readonly string _goldbergPath = Path.Combine(Directory.GetCurrentDirectory(), "goldberg"); + private static readonly string GlobalSettingsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Goldberg SteamEmu Saves"); + private readonly string _accountNamePath = Path.Combine(GlobalSettingsPath, "settings/account_name.txt"); private readonly string _userSteamIdPath = Path.Combine(GlobalSettingsPath, "settings/user_steam_id.txt"); + private readonly string _languagePath = Path.Combine(GlobalSettingsPath, "settings/language.txt"); + private readonly List _interfaceNames = new List { "SteamClient", @@ -68,33 +74,53 @@ namespace GoldbergGUI.Core.Services "STEAMVIDEO_INTERFACE_V" }; - // Call Download // Get global settings - public async Task<(string accountName, long userSteamId)> Initialize(IMvxLog log) + public async Task<(string accountName, long userSteamId, string language)> Initialize(IMvxLog log) { _log = log; var download = await Download().ConfigureAwait(false); if (download) await Extract(_goldbergZipPath).ConfigureAwait(false); + return await GetGlobalSettings().ConfigureAwait(false); + } + public async Task<(string accountName, long steamId, string language)> GetGlobalSettings() + { var accountName = "Account name..."; long steamId = -1; + var language = DefaultLanguage; await Task.Run(() => { if (File.Exists(_accountNamePath)) accountName = File.ReadLines(_accountNamePath).First().Trim(); if (File.Exists(_userSteamIdPath) && - !long.TryParse(File.ReadLines(_userSteamIdPath).First().Trim(), out steamId)) + !long.TryParse(File.ReadLines(_userSteamIdPath).First().Trim(), out steamId) && + steamId < 76561197960265729 && steamId > 76561202255233023) _log.Error("Invalid User Steam ID!"); + if (File.Exists(_languagePath)) language = File.ReadLines(_languagePath).First().Trim(); }).ConfigureAwait(false); + return (accountName, steamId, language); + } - return (accountName, steamId); + public async Task SetGlobalSettings(string accountName, long userSteamId, string language) + { + if (accountName != null && accountName != "Account name...") + await File.WriteAllTextAsync(_accountNamePath, accountName).ConfigureAwait(false); + else + await File.WriteAllTextAsync(_accountNamePath, "Goldberg").ConfigureAwait(false); + if (userSteamId >= 76561197960265729 && userSteamId <= 76561202255233023) + await File.WriteAllTextAsync(_userSteamIdPath, userSteamId.ToString()).ConfigureAwait(false); + else + await Task.Run(() => File.Delete(_userSteamIdPath)).ConfigureAwait(false); + if (language != null) + await File.WriteAllTextAsync(_languagePath, language).ConfigureAwait(false); + else + await File.WriteAllTextAsync(_languagePath, DefaultLanguage).ConfigureAwait(false); } // If first time, call GenerateInterfaces // else try to read config - public async Task<(int appId, List dlcList, bool offline, bool disableNetworking, bool disableOverlay)> - Read(string path) + public async Task Read(string path) { var appId = -1; var dlcList = new List(); @@ -104,6 +130,7 @@ namespace GoldbergGUI.Core.Services await Task.Run(() => int.TryParse(File.ReadLines(steamAppidTxt).First().Trim(), out appId)) .ConfigureAwait(false); } + var dlcTxt = Path.Combine(path, "steam_settings", "DLC.txt"); if (File.Exists(dlcTxt)) { @@ -113,23 +140,29 @@ namespace GoldbergGUI.Core.Services { var match = expression.Match(line); if (match.Success) - dlcList.Add(new SteamApp {AppId = Convert.ToInt32(match.Groups["id"].Value), - Name = match.Groups["name"].Value}); + dlcList.Add(new SteamApp + { + AppId = Convert.ToInt32(match.Groups["id"].Value), + Name = match.Groups["name"].Value + }); } } - return (appId, dlcList, - File.Exists(Path.Combine(path, "steam_settings", "offline.txt")), - File.Exists(Path.Combine(path, "steam_settings", "disable_networking.txt")), - File.Exists(Path.Combine(path, "steam_settings", "disable_overlay.txt")) - ); + + return new GoldbergConfiguration + { + AppId = appId, + DlcList = dlcList, + Offline = File.Exists(Path.Combine(path, "steam_settings", "offline.txt")), + DisableNetworking = File.Exists(Path.Combine(path, "steam_settings", "disable_networking.txt")), + DisableOverlay = File.Exists(Path.Combine(path, "steam_settings", "disable_overlay.txt")) + }; } // If first time, rename original SteamAPI DLL to steam_api(64)_o.dll // If not, rename current SteamAPI DLL to steam_api(64).dll.backup // Copy Goldberg DLL to path // Save configuration files - public async Task Save(string path, int appId, List dlcList, - bool offline, bool disableNetworking, bool disableOverlay) + public async Task Save(string path, GoldbergConfiguration c) { // DLL setup const string x86Name = "steam_api"; @@ -149,15 +182,15 @@ namespace GoldbergGUI.Core.Services { Directory.CreateDirectory(Path.Combine(path, "steam_settings")); } - + // create steam_appid.txt - await File.WriteAllTextAsync(Path.Combine(path, "steam_appid.txt"), appId.ToString()).ConfigureAwait(false); - + await File.WriteAllTextAsync(Path.Combine(path, "steam_appid.txt"), c.AppId.ToString()).ConfigureAwait(false); + // DLC - if (dlcList.Count > 0) + if (c.DlcList.Count > 0) { var dlcString = ""; - dlcList.ForEach(x => dlcString += $"{x}\n"); + c.DlcList.ForEach(x => dlcString += $"{x}\n"); await File.WriteAllTextAsync(Path.Combine(path, "steam_settings", "DLC.txt"), dlcString) .ConfigureAwait(false); } @@ -166,37 +199,39 @@ namespace GoldbergGUI.Core.Services if (File.Exists(Path.Combine(path, "steam_settings", "DLC.txt"))) File.Delete(Path.Combine(path, "steam_settings", "DLC.txt")); } - + // Offline - if (offline) + if (c.Offline) { - await File.Create(Path.Combine(path, "steam_settings", "offline.txt")).DisposeAsync().ConfigureAwait(false); + await File.Create(Path.Combine(path, "steam_settings", "offline.txt")).DisposeAsync() + .ConfigureAwait(false); } else { File.Delete(Path.Combine(path, "steam_settings", "offline.txt")); } - + // Disable Networking - if (disableNetworking) + if (c.DisableNetworking) { - await File.Create(Path.Combine(path, "steam_settings", "disable_networking.txt")).DisposeAsync().ConfigureAwait(false); + await File.Create(Path.Combine(path, "steam_settings", "disable_networking.txt")).DisposeAsync() + .ConfigureAwait(false); } else { File.Delete(Path.Combine(path, "steam_settings", "disable_networking.txt")); } - + // Disable Overlay - if (disableOverlay) + if (c.DisableOverlay) { - await File.Create(Path.Combine(path, "steam_settings", "disable_overlay.txt")).DisposeAsync().ConfigureAwait(false); + await File.Create(Path.Combine(path, "steam_settings", "disable_overlay.txt")).DisposeAsync() + .ConfigureAwait(false); } else { File.Delete(Path.Combine(path, "steam_settings", "disable_overlay.txt")); } - } private void CopyDllFiles(string path, string name) @@ -205,7 +240,7 @@ namespace GoldbergGUI.Core.Services var originalDll = Path.Combine(path, $"{name}_o.dll"); var guiBackup = Path.Combine(path, $"{name}.dll.GOLDBERGGUIBACKUP"); var goldbergDll = Path.Combine(_goldbergPath, $"{name}.dll"); - + if (!File.Exists(originalDll)) File.Move(steamApiDll, originalDll); else @@ -213,6 +248,7 @@ namespace GoldbergGUI.Core.Services File.Move(steamApiDll, guiBackup, true); File.SetAttributes(guiBackup, FileAttributes.Hidden); } + File.Copy(goldbergDll, steamApiDll); } @@ -222,7 +258,7 @@ namespace GoldbergGUI.Core.Services var steamAppIdTxtExists = File.Exists(Path.Combine(path, "steam_appid.txt")); return steamSettingsDirExists && steamAppIdTxtExists; } - + // Get webpage // Get job id, compare with local if exists, save it if false or missing // Get latest archive if mismatch, call Extract @@ -313,6 +349,7 @@ namespace GoldbergGUI.Core.Services FindInterfaces(ref result, dllContent, new Regex("STEAMCONTROLLER_INTERFACE_VERSION")); } } + var dirPath = Path.GetDirectoryName(filePath); if (dirPath == null) return; await using var destination = File.CreateText(dirPath + "/steam_interfaces.txt"); @@ -322,6 +359,37 @@ namespace GoldbergGUI.Core.Services } } + public List Languages() => new List + { + DefaultLanguage, + "arabic", + "bulgarian", + "schinese", + "tchinese", + "czech", + "danish", + "dutch", + "finnish", + "french", + "german", + "greek", + "hungarian", + "italian", + "japanese", + "koreana", + "norwegian", + "polish", + "portuguese", + "brazilian", + "romanian", + "russian", + "spanish", + "swedish", + "thai", + "turkish", + "ukrainian" + }; + private static bool FindInterfaces(ref HashSet result, string dllContent, Regex regex) { var success = false; @@ -332,6 +400,7 @@ namespace GoldbergGUI.Core.Services //result += $@"{match.Value}\n"; result.Add(match.Value); } + return success; } } diff --git a/GoldbergGUI.Core/Services/SteamService.cs b/GoldbergGUI.Core/Services/SteamService.cs index dc111a3..e05a41e 100644 --- a/GoldbergGUI.Core/Services/SteamService.cs +++ b/GoldbergGUI.Core/Services/SteamService.cs @@ -30,8 +30,12 @@ namespace GoldbergGUI.Core.Services // ReSharper disable once UnusedType.Global public class SteamService : ISteamService { - private const string CachePath = "steamapps.json"; - private const string SteamUri = "https://api.steampowered.com/ISteamApps/GetAppList/v2/"; + private const string CachePath1 = "steamapps.json"; + //private const string CachePath1 = "steamapps_games.json"; + //private const string CachePath2 = "steamapps_dlc.json"; + private const string SteamUri1 = "https://api.steampowered.com/ISteamApps/GetAppList/v2/"; + //private const string SteamUri1 = "https://api.steampowered.com/IStoreService/GetAppList/v1/?include_games=1&key="; + //private const string SteamUri2 = "https://api.steampowered.com/IStoreService/GetAppList/v1/?include_games=0&include_dlc=1&key="; private const string UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " + @@ -42,33 +46,50 @@ namespace GoldbergGUI.Core.Services public async Task Initialize(IMvxLog log) { + var secrets = new Secrets(); _log = log; _log.Info("Updating cache..."); - var updateNeeded = DateTime.Now.Subtract(File.GetLastWriteTimeUtc(CachePath)).TotalDays >= 1; + var updateNeeded = DateTime.Now.Subtract(File.GetLastWriteTimeUtc(CachePath1)).TotalDays >= 1; + var cacheString = await GetCache(updateNeeded, SteamUri1, CachePath1).ConfigureAwait(false); + SteamApps steamApps; + try + { + steamApps = JsonSerializer.Deserialize(cacheString); + } + catch (JsonException) + { + cacheString = await GetCache(true, SteamUri1, CachePath1).ConfigureAwait(false); + steamApps = JsonSerializer.Deserialize(cacheString); + } + _cache = new HashSet(steamApps.AppList.Apps); + _log.Info("Loaded cache into memory!"); + } + + private async Task GetCache(bool updateNeeded, string steamUri, string cachePath) + { + var secrets = new Secrets(); string cacheString; if (updateNeeded) { _log.Info("Getting content from API..."); var client = new HttpClient(); - var httpCall = client.GetAsync(SteamUri); + var httpCall = client.GetAsync(steamUri + secrets.SteamWebApiKey()); var response = await httpCall.ConfigureAwait(false); var readAsStringAsync = response.Content.ReadAsStringAsync(); var responseBody = await readAsStringAsync.ConfigureAwait(false); _log.Info("Got content from API successfully. Writing to file..."); - await File.WriteAllTextAsync(CachePath, responseBody, Encoding.UTF8).ConfigureAwait(false); + await File.WriteAllTextAsync(cachePath, responseBody, Encoding.UTF8).ConfigureAwait(false); cacheString = responseBody; _log.Info("Cache written to file successfully."); } else { _log.Info("Cache already up to date!"); - cacheString = await File.ReadAllTextAsync(CachePath).ConfigureAwait(false); + cacheString = await File.ReadAllTextAsync(cachePath).ConfigureAwait(false); } - var steamApps = JsonSerializer.Deserialize(cacheString); - _cache = new HashSet(steamApps.AppList.Apps); - _log.Info("Loaded cache into memory!"); + return cacheString; } public IEnumerable GetListOfAppsByName(string name) diff --git a/GoldbergGUI.Core/ViewModels/MainViewModel.cs b/GoldbergGUI.Core/ViewModels/MainViewModel.cs index 317ee1c..1f2ae71 100644 --- a/GoldbergGUI.Core/ViewModels/MainViewModel.cs +++ b/GoldbergGUI.Core/ViewModels/MainViewModel.cs @@ -1,17 +1,18 @@ -using System.Collections.Generic; - using System.Collections.ObjectModel; - using System.IO; - using System.Linq; - using System.Threading.Tasks; - using GoldbergGUI.Core.Models; - using GoldbergGUI.Core.Services; - using Microsoft.Win32; - using MvvmCross.Commands; - using MvvmCross.Logging; - using MvvmCross.Navigation; - using MvvmCross.ViewModels; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using GoldbergGUI.Core.Models; +using GoldbergGUI.Core.Services; +using Microsoft.Win32; +using MvvmCross.Commands; +using MvvmCross.Logging; +using MvvmCross.Navigation; +using MvvmCross.ViewModels; - namespace GoldbergGUI.Core.ViewModels +namespace GoldbergGUI.Core.ViewModels { // ReSharper disable once ClassNeverInstantiated.Global public class MainViewModel : MvxViewModel @@ -35,8 +36,10 @@ private readonly IMvxLog _log; private bool _mainWindowEnabled; private bool _goldbergApplied; + private ObservableCollection _steamLanguages; + private string _selectedLanguage; - public MainViewModel(ISteamService steam, IGoldbergService goldberg, IMvxLogProvider logProvider, + public MainViewModel(ISteamService steam, IGoldbergService goldberg, IMvxLogProvider logProvider, IMvxNavigationService navigationService) { _steam = steam; @@ -50,24 +53,37 @@ base.Prepare(); Task.Run(async () => { + //var errorDuringInit = false; MainWindowEnabled = false; - ResetForm(); - await _steam.Initialize(_log).ConfigureAwait(false); - var (accountName, userSteamId) = await _goldberg.Initialize(_log).ConfigureAwait(false); - AccountName = accountName; - SteamId = userSteamId; + try + { + SteamLanguages = new ObservableCollection(_goldberg.Languages()); + ResetForm(); + await _steam.Initialize(_log).ConfigureAwait(false); + var (accountName, userSteamId, language) = + await _goldberg.Initialize(_log).ConfigureAwait(false); + AccountName = accountName; + SteamId = userSteamId; + SelectedLanguage = language; + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + //errorDuringInit = true; + } + MainWindowEnabled = true; }); } public override async Task Initialize() { - _log.Info("Init"); await base.Initialize().ConfigureAwait(false); } // PROPERTIES // - + public string DllPath { get => _dllPath; @@ -192,7 +208,28 @@ } public bool DllSelected => !DllPath.Contains("Path to game's steam_api(64).dll"); - + + public ObservableCollection SteamLanguages + { + get => _steamLanguages; + set + { + _steamLanguages = value; + RaisePropertyChanged(() => SteamLanguages); + } + } + + public string SelectedLanguage + { + get => _selectedLanguage; + set + { + _selectedLanguage = value; + RaisePropertyChanged(() => SelectedLanguage); + //MyLogger.Log.Debug($"Lang: {value}"); + } + } + // COMMANDS // public IMvxCommand OpenFileCommand => new MvxAsyncCommand(OpenFile); @@ -211,8 +248,8 @@ _log.Warn("File selection canceled."); return; } + DllPath = dialog.FileName; - //_goldberg.GenerateInterfacesFile(filePath); await ReadConfig().ConfigureAwait(false); } @@ -225,6 +262,7 @@ _log.Error("No game name entered!"); return; } + MainWindowEnabled = false; var appByName = _steam.GetAppByName(_gameName); if (appByName != null) @@ -253,6 +291,7 @@ } } } + MainWindowEnabled = true; } @@ -265,6 +304,7 @@ _log.Error("Invalid Steam App!"); return; } + var steamApp = await Task.Run(() => _steam.GetAppById(AppId)).ConfigureAwait(false); if (steamApp != null) GameName = steamApp.Name; } @@ -278,8 +318,10 @@ _log.Error("Invalid Steam App!"); return; } + MainWindowEnabled = false; - var listOfDlc = await _steam.GetListOfDlc(new SteamApp {AppId = AppId, Name = GameName}, true).ConfigureAwait(false); + var listOfDlc = await _steam.GetListOfDlc(new SteamApp {AppId = AppId, Name = GameName}, true) + .ConfigureAwait(false); DLCs = new MvxObservableCollection(listOfDlc); MainWindowEnabled = true; } @@ -288,20 +330,25 @@ private async Task SaveConfig() { + await _goldberg.SetGlobalSettings(AccountName, SteamId, SelectedLanguage).ConfigureAwait(false); if (!DllSelected) { _log.Error("No DLL selected!"); return; } + _log.Info("Saving..."); if (!GetDllPathDir(out var dirPath)) return; MainWindowEnabled = false; - await _goldberg.Save(dirPath, - AppId, - DLCs.ToList(), - Offline, - DisableNetworking, - DisableOverlay).ConfigureAwait(false); + await _goldberg.Save(dirPath, new GoldbergConfiguration + { + AppId = AppId, + DlcList = DLCs.ToList(), + Offline = Offline, + DisableNetworking = DisableNetworking, + DisableOverlay = DisableOverlay + } + ).ConfigureAwait(false); GoldbergApplied = _goldberg.GoldbergApplied(dirPath); MainWindowEnabled = true; } @@ -310,17 +357,19 @@ private async Task ResetConfig() { + (AccountName, SteamId, SelectedLanguage) = await _goldberg.GetGlobalSettings().ConfigureAwait(false); if (!DllSelected) { _log.Error("No DLL selected!"); return; } + _log.Info("Reset form..."); MainWindowEnabled = false; await ReadConfig().ConfigureAwait(false); MainWindowEnabled = true; } - + public IMvxCommand GenerateSteamInterfacesCommand => new MvxAsyncCommand(GenerateSteamInterfaces); private async Task GenerateSteamInterfaces() @@ -330,20 +379,22 @@ _log.Error("No DLL selected!"); return; } + _log.Info("Generate steam_interfaces.txt..."); MainWindowEnabled = false; GetDllPathDir(out var dirPath); if (File.Exists(Path.Combine(dirPath, "steam_api_o.dll"))) - await _goldberg.GenerateInterfacesFile(Path.Combine(dirPath, "steam_api_o.dll")).ConfigureAwait(false); + await _goldberg.GenerateInterfacesFile(Path.Combine(dirPath, "steam_api_o.dll")).ConfigureAwait(false); else if (File.Exists(Path.Combine(dirPath, "steam_api64_o.dll"))) - await _goldberg.GenerateInterfacesFile(Path.Combine(dirPath, "steam_api64_o.dll")).ConfigureAwait(false); + await _goldberg.GenerateInterfacesFile(Path.Combine(dirPath, "steam_api64_o.dll")) + .ConfigureAwait(false); else await _goldberg.GenerateInterfacesFile(DllPath).ConfigureAwait(false); await RaisePropertyChanged(() => SteamInterfacesTxtExists).ConfigureAwait(false); MainWindowEnabled = true; } - + // OTHER METHODS // - + private void ResetForm() { DllPath = "Path to game's steam_api(64).dll..."; @@ -361,15 +412,22 @@ { if (!GetDllPathDir(out var dirPath)) return; MainWindowEnabled = false; - List dlcList; - (AppId, dlcList, Offline, DisableNetworking, DisableOverlay) = - await _goldberg.Read(dirPath).ConfigureAwait(false); - DLCs = new ObservableCollection(dlcList); + var config = await _goldberg.Read(dirPath).ConfigureAwait(false); + SetFormFromConfig(config); GoldbergApplied = _goldberg.GoldbergApplied(dirPath); await RaisePropertyChanged(() => SteamInterfacesTxtExists).ConfigureAwait(false); MainWindowEnabled = true; } + private void SetFormFromConfig(GoldbergConfiguration config) + { + AppId = config.AppId; + DLCs = new ObservableCollection(config.DlcList); + Offline = config.Offline; + DisableNetworking = config.DisableNetworking; + DisableOverlay = config.DisableOverlay; + } + private bool GetDllPathDir(out string dirPath) { if (!DllSelected) @@ -381,7 +439,7 @@ dirPath = Path.GetDirectoryName(DllPath); if (dirPath != null) return true; - + _log.Error($"Invalid directory for {DllPath}."); return false; } diff --git a/GoldbergGUI.WPF/Views/MainView.xaml b/GoldbergGUI.WPF/Views/MainView.xaml index f0fe4e4..ccb2880 100644 --- a/GoldbergGUI.WPF/Views/MainView.xaml +++ b/GoldbergGUI.WPF/Views/MainView.xaml @@ -80,12 +80,16 @@ +