diff --git a/Wabbajack.App/Controls/BrowseItemViewModel.cs b/Wabbajack.App/Controls/BrowseItemViewModel.cs index 6d1acef5..beb0bad1 100644 --- a/Wabbajack.App/Controls/BrowseItemViewModel.cs +++ b/Wabbajack.App/Controls/BrowseItemViewModel.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net.Http; using System.Reactive; using System.Reactive.Linq; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Avalonia.Media.Imaging; @@ -17,6 +18,7 @@ using Wabbajack.App.ViewModels; using Wabbajack.Common; using Wabbajack.Downloaders; using Wabbajack.DTOs; +using Wabbajack.DTOs.JsonConverters; using Wabbajack.Installer; using Wabbajack.Paths; using Wabbajack.Paths.IO; @@ -43,6 +45,7 @@ namespace Wabbajack.App.Controls private readonly DownloadDispatcher _dispatcher; private readonly ILogger _logger; private readonly IResource _downloadLimiter; + private readonly DTOSerializer _dtos; public string Title => _metadata.ImageContainsTitle ? "" : _metadata.Title; public string MachineURL => _metadata.Links.MachineURL; @@ -74,7 +77,7 @@ namespace Wabbajack.App.Controls public BrowseItemViewModel(ModlistMetadata metadata, ModListSummary summary, HttpClient client, IResource limiter, FileHashCache hashCache, Configuration configuration, DownloadDispatcher dispatcher, IResource downloadLimiter, GameLocator gameLocator, - ILogger logger) + DTOSerializer dtos, ILogger logger) { Activator = new ViewModelActivator(); _metadata = metadata; @@ -86,6 +89,8 @@ namespace Wabbajack.App.Controls _dispatcher = dispatcher; _downloadLimiter = downloadLimiter; _logger = logger; + _dtos = dtos; + var haveGame = gameLocator.IsInstalled(_metadata.Game); Tags = metadata.tags .Select(t => new TagViewModel(t, "ModList")) @@ -150,6 +155,9 @@ namespace Wabbajack.App.Controls _hashCache.FileHashWriteCache(ModListLocation, hash); + var metadataPath = ModListLocation.WithExtension(Ext.MetaData); + await metadataPath.WriteAllTextAsync(_dtos.Serialize(_metadata)); + await UpdateState(); } diff --git a/Wabbajack.App/Messages/ConfigureLauncher.cs b/Wabbajack.App/Messages/ConfigureLauncher.cs new file mode 100644 index 00000000..1cbc0278 --- /dev/null +++ b/Wabbajack.App/Messages/ConfigureLauncher.cs @@ -0,0 +1,9 @@ +using Wabbajack.Paths; + +namespace Wabbajack.App.Messages +{ + public record ConfigureLauncher(AbsolutePath InstallFolder) + { + + } +} \ No newline at end of file diff --git a/Wabbajack.App/Messages/StartInstallation.cs b/Wabbajack.App/Messages/StartInstallation.cs index 5b72893e..15b5f3ef 100644 --- a/Wabbajack.App/Messages/StartInstallation.cs +++ b/Wabbajack.App/Messages/StartInstallation.cs @@ -3,7 +3,7 @@ using Wabbajack.Paths; namespace Wabbajack.App.Messages { - public record StartInstallation(AbsolutePath ModListPath, AbsolutePath Install, AbsolutePath Download) + public record StartInstallation(AbsolutePath ModListPath, AbsolutePath Install, AbsolutePath Download, ModlistMetadata? Metadata) { } } \ No newline at end of file diff --git a/Wabbajack.App/Models/InstallationStateManager.cs b/Wabbajack.App/Models/InstallationStateManager.cs index b95ac7b4..414a149e 100644 --- a/Wabbajack.App/Models/InstallationStateManager.cs +++ b/Wabbajack.App/Models/InstallationStateManager.cs @@ -73,5 +73,10 @@ namespace Wabbajack.App.Models { return (await GetAll()).Settings.FirstOrDefault(f => f.ModList == modListPath); } + + public async Task GetByInstallFolder(AbsolutePath folder) + { + return (await GetAll()).Settings.FirstOrDefault(f => f.Install == folder); + } } } \ No newline at end of file diff --git a/Wabbajack.App/Screens/BrowseViewModel.cs b/Wabbajack.App/Screens/BrowseViewModel.cs index b536c7aa..f324318e 100644 --- a/Wabbajack.App/Screens/BrowseViewModel.cs +++ b/Wabbajack.App/Screens/BrowseViewModel.cs @@ -24,6 +24,7 @@ using Wabbajack.Networking.WabbajackClientApi; using DynamicData.Binding; using Microsoft.Extensions.DependencyInjection; using Wabbajack.Downloaders; +using Wabbajack.DTOs.JsonConverters; using Wabbajack.Installer; using Wabbajack.Paths; using Wabbajack.Paths.IO; @@ -52,6 +53,7 @@ namespace Wabbajack.App.Screens private SourceCache _gamesList = new(x => x.Name); public readonly ReadOnlyObservableCollection _filteredGamesList; private readonly GameLocator _gameLocator; + private readonly DTOSerializer _dtos; public ReadOnlyObservableCollection GamesList => _filteredGamesList; [Reactive] @@ -67,7 +69,7 @@ namespace Wabbajack.App.Screens [Reactive] public bool ShowNSFW { get; set; } = false; public BrowseViewModel(ILogger logger, Client wjClient, HttpClient httpClient, IResource limiter, FileHashCache hashCache, - IResource dispatcherLimiter, DownloadDispatcher dispatcher, GameLocator gameLocator, Configuration configuration) + IResource dispatcherLimiter, DownloadDispatcher dispatcher, GameLocator gameLocator, DTOSerializer dtos, Configuration configuration) { Activator = new ViewModelActivator(); _wjClient = wjClient; @@ -79,6 +81,7 @@ namespace Wabbajack.App.Screens _dispatcher = dispatcher; _dispatcherLimiter = dispatcherLimiter; _gameLocator = gameLocator; + _dtos = dtos; IObservable> searchTextPredicates = this.ObservableForProperty(vm => vm.SearchText) @@ -198,7 +201,7 @@ namespace Wabbajack.App.Screens summary = new ModListSummary(); } - return new BrowseItemViewModel(m, summary, _httpClient, _limiter, _hashCache, _configuration, _dispatcher, _dispatcherLimiter, _gameLocator, _logger); + return new BrowseItemViewModel(m, summary, _httpClient, _limiter, _hashCache, _configuration, _dispatcher, _dispatcherLimiter, _gameLocator, _dtos, _logger); }); _modLists.Edit(lsts => diff --git a/Wabbajack.App/Screens/LauncherView.axaml b/Wabbajack.App/Screens/LauncherView.axaml new file mode 100644 index 00000000..274f83a9 --- /dev/null +++ b/Wabbajack.App/Screens/LauncherView.axaml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wabbajack.App/Screens/LauncherView.axaml.cs b/Wabbajack.App/Screens/LauncherView.axaml.cs new file mode 100644 index 00000000..96f4477c --- /dev/null +++ b/Wabbajack.App/Screens/LauncherView.axaml.cs @@ -0,0 +1,33 @@ +using System.Reactive.Disposables; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using ReactiveUI; +using Wabbajack.App.Views; + +namespace Wabbajack.App.Screens +{ + public partial class LauncherView : ScreenBase + { + public LauncherView() + { + InitializeComponent(); + this.WhenActivated(disposables => + { + this.OneWayBind(ViewModel, vm => vm.Image, view => view.ModListImage.Source) + .DisposeWith(disposables); + + this.OneWayBind(ViewModel, vm => vm.Title, view => view.ModList.Text) + .DisposeWith(disposables); + + this.OneWayBind(ViewModel, vm => vm.InstallFolder, view => view.InstallPath.Text, + v => v.ToString()) + .DisposeWith(disposables); + + this.BindCommand(ViewModel, vm => vm.PlayButton, view => view.PlayGame.Button) + .DisposeWith(disposables); + }); + } + + } +} \ No newline at end of file diff --git a/Wabbajack.App/Screens/LauncherViewModel.cs b/Wabbajack.App/Screens/LauncherViewModel.cs new file mode 100644 index 00000000..cde96f2d --- /dev/null +++ b/Wabbajack.App/Screens/LauncherViewModel.cs @@ -0,0 +1,102 @@ +using System.Diagnostics; +using System.Linq; +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Avalonia.Media.Imaging; +using GameFinder.StoreHandlers.Origin.DTO; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Logging; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using Wabbajack.App.Extensions; +using Wabbajack.App.Messages; +using Wabbajack.App.Models; +using Wabbajack.App.ViewModels; +using Wabbajack.Common; +using Wabbajack.DTOs; +using Wabbajack.DTOs.SavedSettings; +using Wabbajack.Paths; +using Wabbajack.Paths.IO; + +namespace Wabbajack.App.Screens +{ + public class LauncherViewModel : ViewModelBase, IActivatableViewModel, IReceiver + { + + [Reactive] + public AbsolutePath InstallFolder { get; set; } + + [Reactive] + public IBitmap Image { get; set; } + + [Reactive] + public InstallationConfigurationSetting? Setting { get; set; } + + [Reactive] + public string Title { get; set; } + + public ReactiveCommand PlayButton; + private readonly ILogger _logger; + + public LauncherViewModel(ILogger logger, InstallationStateManager manager) + { + Activator = new ViewModelActivator(); + PlayButton = ReactiveCommand.Create(() => + { + StartGame().FireAndForget(); + }); + _logger = logger; + + this.WhenActivated(disposables => + { + this.WhenAnyValue(v => v.InstallFolder) + .SelectAsync(disposables, async folder => await manager.GetByInstallFolder(folder)) + .ObserveOn(RxApp.MainThreadScheduler) + .Where(v => v != null) + .BindTo(this, vm => vm.Setting) + .DisposeWith(disposables); + + this.WhenAnyValue(v => v.Setting) + .Where(v => v != default) + .Select(v => new Bitmap((v!.Image).ToString())) + .BindTo(this, vm => vm.Image) + .DisposeWith(disposables); + + this.WhenAnyValue(v => v.Setting) + .Where(v => v is { Metadata: { } }) + .Select(v => $"{v!.Metadata!.Title} v{v!.Metadata.Version}") + .BindTo(this, vm => vm.Title) + .DisposeWith(disposables); + + }); + } + + private async Task StartGame() + { + var mo2Path = InstallFolder.Combine("ModOrganizer.exe"); + var gamePath = GameRegistry.Games.Values.Select(g => g.MainExecutable) + .Where(ge => ge != null) + .Select(ge => InstallFolder.Combine(ge!)) + .FirstOrDefault(ge => ge.FileExists()); + if (mo2Path.FileExists()) + { + Process.Start(mo2Path.ToString()); + } + else if (gamePath.FileExists()) + { + Process.Start(gamePath.ToString()); + } + else + { + _logger.LogError("No way to launch game, no acceptable executable found"); + } + } + + public void Receive(ConfigureLauncher val) + { + InstallFolder = val.InstallFolder; + } + } +} \ No newline at end of file diff --git a/Wabbajack.App/Views/StandardInstallationView.axaml b/Wabbajack.App/Screens/StandardInstallationView.axaml similarity index 95% rename from Wabbajack.App/Views/StandardInstallationView.axaml rename to Wabbajack.App/Screens/StandardInstallationView.axaml index b0908d0e..3dcad5bc 100644 --- a/Wabbajack.App/Views/StandardInstallationView.axaml +++ b/Wabbajack.App/Screens/StandardInstallationView.axaml @@ -12,7 +12,7 @@ - + diff --git a/Wabbajack.App/Views/StandardInstallationView.axaml.cs b/Wabbajack.App/Screens/StandardInstallationView.axaml.cs similarity index 100% rename from Wabbajack.App/Views/StandardInstallationView.axaml.cs rename to Wabbajack.App/Screens/StandardInstallationView.axaml.cs diff --git a/Wabbajack.App/ViewModels/StandardInstallationViewModel.cs b/Wabbajack.App/Screens/StandardInstallationViewModel.cs similarity index 81% rename from Wabbajack.App/ViewModels/StandardInstallationViewModel.cs rename to Wabbajack.App/Screens/StandardInstallationViewModel.cs index dc41e515..ead47d73 100644 --- a/Wabbajack.App/ViewModels/StandardInstallationViewModel.cs +++ b/Wabbajack.App/Screens/StandardInstallationViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Linq; using System.Net.Http; using System.Reactive; @@ -13,12 +14,17 @@ using Microsoft.Extensions.Logging; using ReactiveUI; using ReactiveUI.Fody.Helpers; using Wabbajack.App.Messages; +using Wabbajack.App.Models; +using Wabbajack.App.Screens; +using Wabbajack.App.Utilities; using Wabbajack.App.ViewModels.SubViewModels; using Wabbajack.Common; using Wabbajack.DTOs; using Wabbajack.DTOs.DownloadStates; using Wabbajack.DTOs.JsonConverters; +using Wabbajack.DTOs.SavedSettings; using Wabbajack.Installer; +using Wabbajack.Paths.IO; using Wabbajack.RateLimiter; namespace Wabbajack.App.ViewModels @@ -36,6 +42,7 @@ namespace Wabbajack.App.ViewModels private readonly HttpClient _httpClient; private Timer _slideTimer; private int _currentSlideIndex; + private readonly InstallationStateManager _installStateManager; [Reactive] public SlideViewModel Slide { get; set; } @@ -58,13 +65,15 @@ namespace Wabbajack.App.ViewModels [Reactive] public Percent StepsProgress { get; set; } = Percent.Zero; [Reactive] public Percent StepProgress { get; set; } = Percent.Zero; - public StandardInstallationViewModel(ILogger logger, IServiceProvider provider, GameLocator locator, DTOSerializer dtos, HttpClient httpClient) + public StandardInstallationViewModel(ILogger logger, IServiceProvider provider, GameLocator locator, DTOSerializer dtos, + HttpClient httpClient, InstallationStateManager manager) { _provider = provider; _locator = locator; _logger = logger; _dtos = dtos; _httpClient = httpClient; + _installStateManager = manager; Activator = new ViewModelActivator(); this.WhenActivated(disposables => { @@ -142,6 +151,7 @@ namespace Wabbajack.App.ViewModels _config.Downloads = msg.Download; _config.Install = msg.Install; _config.ModlistArchive = msg.ModListPath; + _config.Metadata = msg.Metadata; _logger.LogInformation("Loading ModList Data"); _config.ModList = await StandardInstaller.LoadFromFile(_dtos, msg.ModListPath); @@ -180,7 +190,34 @@ namespace Wabbajack.App.ViewModels }; _logger.LogInformation("Installer created, starting the installation process"); - await _installer.Begin(CancellationToken.None); + var result = await _installer.Begin(CancellationToken.None); + + if (result) + { + await SaveConfigAndContinue(_config); + } + } + + private async Task SaveConfigAndContinue(InstallerConfiguration config) + { + var path = config.Install.Combine("modlist-image.png"); + { + var image = await ModListUtilities.GetModListImageStream(config.ModlistArchive); + await using var os = path.Open(FileMode.Create, FileAccess.Write); + await image.CopyToAsync(os); + } + + await _installStateManager.SetLastState(new InstallationConfigurationSetting + { + Downloads = config.Downloads, + Install = config.Install, + Metadata = config.Metadata, + ModList = config.ModlistArchive, + Image = path + }); + + MessageBus.Instance.Send(new ConfigureLauncher(config.Install)); + MessageBus.Instance.Send(new NavigateTo(typeof(LauncherViewModel))); } } } \ No newline at end of file diff --git a/Wabbajack.App/ServiceExtensions.cs b/Wabbajack.App/ServiceExtensions.cs index 97f6247b..16a69d59 100644 --- a/Wabbajack.App/ServiceExtensions.cs +++ b/Wabbajack.App/ServiceExtensions.cs @@ -46,6 +46,8 @@ namespace Wabbajack.App services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); @@ -56,6 +58,7 @@ namespace Wabbajack.App services.AddAllSingleton(); services.AddAllSingleton(); services.AddAllSingleton(); + services.AddAllSingleton(); // Services services.AddAllSingleton, ManualDownloader>(); diff --git a/Wabbajack.App/Utilities/ModListUtilities.cs b/Wabbajack.App/Utilities/ModListUtilities.cs new file mode 100644 index 00000000..b7072115 --- /dev/null +++ b/Wabbajack.App/Utilities/ModListUtilities.cs @@ -0,0 +1,21 @@ +using System.IO; +using System.IO.Compression; +using System.Threading.Tasks; +using Wabbajack.Common; +using Wabbajack.Paths; +using Wabbajack.Paths.IO; + +namespace Wabbajack.App.Utilities +{ + public class ModListUtilities + { + public static async Task GetModListImageStream(AbsolutePath modList) + { + await using var fs = modList.Open(FileMode.Open, FileAccess.Read, FileShare.Read); + using var ar = new ZipArchive(fs, ZipArchiveMode.Read); + var entry = ar.GetEntry("modlist-image.png"); + await using var stream = entry!.Open(); + return new MemoryStream(await stream.ReadAllAsync()); + } + } +} \ No newline at end of file diff --git a/Wabbajack.App/ViewModels/InstallConfigurationViewModel.cs b/Wabbajack.App/ViewModels/InstallConfigurationViewModel.cs index 01e8a5a0..85c55627 100644 --- a/Wabbajack.App/ViewModels/InstallConfigurationViewModel.cs +++ b/Wabbajack.App/ViewModels/InstallConfigurationViewModel.cs @@ -12,6 +12,7 @@ using ReactiveUI.Validation.Extensions; using Wabbajack.App.Extensions; using Wabbajack.App.Messages; using Wabbajack.App.Models; +using Wabbajack.App.Utilities; using Wabbajack.Common; using Wabbajack.DTOs; using Wabbajack.DTOs.JsonConverters; @@ -63,7 +64,7 @@ namespace Wabbajack.App.ViewModels this.ValidationRule(x => x.Install, p => p.DirectoryExists(), "Install folder file must exist"); this.ValidationRule(x => x.Download, p => p != default, "Download folder must be set"); - BeginCommand = ReactiveCommand.Create(StartInstall, this.IsValid()); + BeginCommand = ReactiveCommand.Create(() => {StartInstall().FireAndForget();}, this.IsValid()); this.WhenAnyValue(t => t.ModListPath) @@ -97,26 +98,30 @@ namespace Wabbajack.App.ViewModels } - private void StartInstall() + private async Task StartInstall() { + ModlistMetadata? metadata = null; + var metadataPath = ModListPath.WithExtension(Ext.MetaData); + if (metadataPath.FileExists()) + { + metadata = _dtos.Deserialize(await metadataPath.ReadAllTextAsync()); + } + _stateManager.SetLastState(new InstallationConfigurationSetting { ModList = ModListPath, Downloads = Download, - Install = Install + Install = Install, + Metadata = metadata }).FireAndForget(); MessageBus.Instance.Send(new NavigateTo(typeof(StandardInstallationViewModel))); - MessageBus.Instance.Send(new StartInstallation(ModListPath, Install, Download)); + MessageBus.Instance.Send(new StartInstallation(ModListPath, Install, Download, metadata)); } private async Task LoadModListImage(AbsolutePath path) { - await using var fs = path.Open(FileMode.Open, FileAccess.Read, FileShare.Read); - using var ar = new ZipArchive(fs, ZipArchiveMode.Read); - var entry = ar.GetEntry("modlist-image.png"); - await using var stream = entry!.Open(); - return new Bitmap(new MemoryStream(await stream.ReadAllAsync())); + return new Bitmap(await ModListUtilities.GetModListImageStream(path)); } private async Task LoadModList(AbsolutePath modlist) diff --git a/Wabbajack.App/ViewModels/MainWindowViewModel.cs b/Wabbajack.App/ViewModels/MainWindowViewModel.cs index 1780b33e..10ae2598 100644 --- a/Wabbajack.App/ViewModels/MainWindowViewModel.cs +++ b/Wabbajack.App/ViewModels/MainWindowViewModel.cs @@ -14,9 +14,11 @@ using ReactiveUI.Fody.Helpers; using ReactiveUI.Validation.Helpers; using Wabbajack.App.Interfaces; using Wabbajack.App.Messages; +using Wabbajack.App.Models; using Wabbajack.App.Screens; using Wabbajack.App.Views; using Wabbajack.Common; +using Wabbajack.Paths.IO; using Wabbajack.RateLimiter; namespace Wabbajack.App.ViewModels @@ -28,6 +30,7 @@ namespace Wabbajack.App.ViewModels private readonly IResource[] _resources; private StatusReport[] _prevReport; private readonly Task _resourcePoller; + private readonly InstallationStateManager _manager; [Reactive] public Control CurrentScreen { get; set; } @@ -44,11 +47,13 @@ namespace Wabbajack.App.ViewModels [Reactive] public string ResourceStatus { get; set; } - public MainWindowViewModel(IEnumerable screens, IEnumerable resources, IServiceProvider provider) + public MainWindowViewModel(IEnumerable screens, IEnumerable resources, IServiceProvider provider, + InstallationStateManager manager) { _provider = provider; _screens = screens; _resources = resources.ToArray(); + _manager = manager; _prevReport = NextReport(); @@ -73,9 +78,26 @@ namespace Wabbajack.App.ViewModels .DisposeWith(disposables); }); - - Receive(new NavigateTo(typeof(ModeSelectionViewModel))); + LoadFirstScreen().FireAndForget(); + + } + + private async Task LoadFirstScreen() + { + var setting = await _manager.GetLastState(); + if (setting.Install != default && setting.Install.DirectoryExists()) + { + BreadCrumbs = + BreadCrumbs.Push((Control)_screens.First(s => s.ViewModelType == typeof(ModeSelectionViewModel))); + + MessageBus.Instance.Send(new ConfigureLauncher(setting.Install)); + Receive(new NavigateTo(typeof(LauncherViewModel))); + } + else + { + Receive(new NavigateTo(typeof(ModeSelectionViewModel))); + } } private StatusReport[] NextReport() diff --git a/Wabbajack.App/Wabbajack.App.csproj b/Wabbajack.App/Wabbajack.App.csproj index feada28e..463439b3 100644 --- a/Wabbajack.App/Wabbajack.App.csproj +++ b/Wabbajack.App/Wabbajack.App.csproj @@ -41,6 +41,10 @@ SettingsView.axaml Code + + StandardInstallationView.axaml + Code + diff --git a/Wabbajack.Common/Ext.cs b/Wabbajack.Common/Ext.cs index 6e9905e8..a1cda415 100644 --- a/Wabbajack.Common/Ext.cs +++ b/Wabbajack.Common/Ext.cs @@ -19,5 +19,6 @@ namespace Wabbajack.Common public static Extension Dds = new(".dds"); public static Extension Json = new(".json"); public static Extension Md = new(".md"); + public static Extension MetaData = new(".metadata"); } } \ No newline at end of file diff --git a/Wabbajack.DTOs/SavedSettings/InstallationSettings.cs b/Wabbajack.DTOs/SavedSettings/InstallationSettings.cs index 7694b78e..525aed52 100644 --- a/Wabbajack.DTOs/SavedSettings/InstallationSettings.cs +++ b/Wabbajack.DTOs/SavedSettings/InstallationSettings.cs @@ -16,5 +16,9 @@ namespace Wabbajack.DTOs.SavedSettings public AbsolutePath ModList { get; set; } public AbsolutePath Install { get; set; } public AbsolutePath Downloads { get; set; } + + public ModlistMetadata? Metadata { get; set; } + + public AbsolutePath Image { get; set; } } } \ No newline at end of file diff --git a/Wabbajack.Installer/InstallerConfiguration.cs b/Wabbajack.Installer/InstallerConfiguration.cs index c3965d5b..2e51e227 100644 --- a/Wabbajack.Installer/InstallerConfiguration.cs +++ b/Wabbajack.Installer/InstallerConfiguration.cs @@ -12,5 +12,7 @@ namespace Wabbajack.Installer public SystemParameters? SystemParameters { get; set; } public Game Game { get; set; } public AbsolutePath GameFolder { get; set; } + + public ModlistMetadata? Metadata { get; set; } } } \ No newline at end of file